Add scoped Agent-mode pipeline as experimental Classic fork.
Wire specialist waves, Brain consolidation, and Classic-compatible reports so Agent mode can run end-to-end via OpenRouter without changing the default Classic path. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
"""Thread-safe per-job blackboard for the Agent pipeline."""
|
||||
|
||||
import copy
|
||||
import json
|
||||
import os
|
||||
import threading
|
||||
from typing import Any, Dict, Iterable, Optional
|
||||
|
||||
|
||||
_COLLECTION_KEYS = {"sheets", "clusters", "findings", "decisions", "rfis"}
|
||||
_MAPPING_KEYS = {"sheet_index", "jurisdiction", "object_graph"}
|
||||
_MEMORY_KEYS = _COLLECTION_KEYS | _MAPPING_KEYS
|
||||
|
||||
|
||||
class ProjectMemory:
|
||||
"""Owns intermediate Agent-mode state and optional debug snapshots."""
|
||||
|
||||
def __init__(self, artifact_dir: Optional[str] = None) -> None:
|
||||
self.artifact_dir = artifact_dir
|
||||
self._lock = threading.RLock()
|
||||
self._data: Dict[str, Any] = {
|
||||
**{key: [] for key in _COLLECTION_KEYS},
|
||||
**{key: {} for key in _MAPPING_KEYS},
|
||||
}
|
||||
if artifact_dir:
|
||||
os.makedirs(artifact_dir, exist_ok=True)
|
||||
|
||||
def replace(self, key: str, value: Any) -> None:
|
||||
"""Replace one named memory section."""
|
||||
self._validate_key(key)
|
||||
with self._lock:
|
||||
self._data[key] = copy.deepcopy(value)
|
||||
|
||||
def append(self, key: str, value: Dict[str, Any]) -> None:
|
||||
"""Append one artifact to a list-backed memory section."""
|
||||
if key not in _COLLECTION_KEYS:
|
||||
raise KeyError(f"{key!r} is not an appendable memory section")
|
||||
with self._lock:
|
||||
self._data[key].append(copy.deepcopy(value))
|
||||
|
||||
def extend(self, key: str, values: Iterable[Dict[str, Any]]) -> None:
|
||||
"""Append several artifacts under one lock."""
|
||||
if key not in _COLLECTION_KEYS:
|
||||
raise KeyError(f"{key!r} is not an appendable memory section")
|
||||
with self._lock:
|
||||
self._data[key].extend(copy.deepcopy(list(values)))
|
||||
|
||||
def snapshot(self) -> Dict[str, Any]:
|
||||
"""Return a detached, JSON-serializable view of current state."""
|
||||
with self._lock:
|
||||
return copy.deepcopy(self._data)
|
||||
|
||||
def dump(self, filename: str = "memory.json") -> Optional[str]:
|
||||
"""Persist a snapshot when this job has an artifact directory."""
|
||||
if not self.artifact_dir:
|
||||
return None
|
||||
path = os.path.join(self.artifact_dir, filename)
|
||||
temp_path = f"{path}.tmp"
|
||||
with open(temp_path, "w", encoding="utf-8") as f:
|
||||
json.dump(self.snapshot(), f, indent=2)
|
||||
os.replace(temp_path, path)
|
||||
return path
|
||||
|
||||
@staticmethod
|
||||
def _validate_key(key: str) -> None:
|
||||
if key not in _MEMORY_KEYS:
|
||||
raise KeyError(f"Unknown project memory section: {key!r}")
|
||||
Reference in New Issue
Block a user