Merge branch 'feat/open-hive' into feature/queen-worker-comm

This commit is contained in:
Timothy
2026-03-01 16:06:43 -08:00
14 changed files with 32 additions and 142 deletions
+1 -1
View File
@@ -761,7 +761,7 @@ class GraphBuilder:
path = self.storage_path / f"{session_id}.json"
if not path.exists():
raise FileNotFoundError(f"Session not found: {session_id}")
return BuildSession.model_validate_json(path.read_text())
return BuildSession.model_validate_json(path.read_text(encoding="utf-8"))
@classmethod
def list_sessions(cls, storage_path: Path | str | None = None) -> list[str]:
+1 -1
View File
@@ -164,7 +164,7 @@ def _read_credential_key_file() -> str | None:
"""Read the credential key from ``~/.hive/secrets/credential_key``."""
try:
if CREDENTIAL_KEY_PATH.is_file():
value = CREDENTIAL_KEY_PATH.read_text().strip()
value = CREDENTIAL_KEY_PATH.read_text(encoding="utf-8").strip()
if value:
return value
except Exception:
+3 -3
View File
@@ -1887,7 +1887,7 @@ def import_from_export(
return json.dumps({"success": False, "error": f"File not found: {agent_json_path}"})
try:
data = json.loads(path.read_text())
data = json.loads(path.read_text(encoding="utf-8"))
except json.JSONDecodeError as e:
return json.dumps({"success": False, "error": f"Invalid JSON: {e}"})
@@ -3009,7 +3009,7 @@ def debug_test(
# Find which file contains the test
test_file = None
for py_file in tests_dir.glob("test_*.py"):
content = py_file.read_text()
content = py_file.read_text(encoding="utf-8")
if f"def {test_name}" in content or f"async def {test_name}" in content:
test_file = py_file
break
@@ -3161,7 +3161,7 @@ def list_tests(
tests = []
for test_file in sorted(tests_dir.glob("test_*.py")):
try:
content = test_file.read_text()
content = test_file.read_text(encoding="utf-8")
tree = ast.parse(content)
# Find all async function definitions that start with "test_"
+2 -2
View File
@@ -428,7 +428,7 @@ def _load_resume_state(
if not cp_path.exists():
return None
try:
cp_data = json.loads(cp_path.read_text())
cp_data = json.loads(cp_path.read_text(encoding="utf-8"))
except (json.JSONDecodeError, OSError):
return None
return {
@@ -444,7 +444,7 @@ def _load_resume_state(
if not state_path.exists():
return None
try:
state_data = json.loads(state_path.read_text())
state_data = json.loads(state_path.read_text(encoding="utf-8"))
except (json.JSONDecodeError, OSError):
return None
progress = state_data.get("progress", {})
+1 -1
View File
@@ -207,7 +207,7 @@ async def handle_resume(request: web.Request) -> web.Response:
return web.json_response({"error": "Session not found"}, status=404)
try:
state = json.loads(state_path.read_text())
state = json.loads(state_path.read_text(encoding="utf-8"))
except (json.JSONDecodeError, OSError) as e:
return web.json_response({"error": f"Failed to read session: {e}"}, status=500)
+1 -1
View File
@@ -79,7 +79,7 @@ async def handle_list_nodes(request: web.Request) -> web.Response:
)
if state_path.exists():
try:
state = json.loads(state_path.read_text())
state = json.loads(state_path.read_text(encoding="utf-8"))
progress = state.get("progress", {})
visit_counts = progress.get("node_visit_counts", {})
failures = progress.get("nodes_with_failures", [])
+5 -5
View File
@@ -369,7 +369,7 @@ async def handle_list_worker_sessions(request: web.Request) -> web.Response:
state_path = d / "state.json"
if state_path.exists():
try:
state = json.loads(state_path.read_text())
state = json.loads(state_path.read_text(encoding="utf-8"))
entry["status"] = state.get("status", "unknown")
entry["started_at"] = state.get("started_at")
entry["completed_at"] = state.get("completed_at")
@@ -408,7 +408,7 @@ async def handle_get_worker_session(request: web.Request) -> web.Response:
return web.json_response({"error": "Session not found"}, status=404)
try:
state = json.loads(state_path.read_text())
state = json.loads(state_path.read_text(encoding="utf-8"))
except (json.JSONDecodeError, OSError) as e:
return web.json_response({"error": f"Failed to read session: {e}"}, status=500)
@@ -436,7 +436,7 @@ async def handle_list_checkpoints(request: web.Request) -> web.Response:
if f.suffix != ".json":
continue
try:
data = json.loads(f.read_text())
data = json.loads(f.read_text(encoding="utf-8"))
checkpoints.append(
{
"checkpoint_id": f.stem,
@@ -546,7 +546,7 @@ async def handle_messages(request: web.Request) -> web.Response:
if part_file.suffix != ".json":
continue
try:
part = json.loads(part_file.read_text())
part = json.loads(part_file.read_text(encoding="utf-8"))
part["_node_id"] = node_dir.name
all_messages.append(part)
except (json.JSONDecodeError, OSError):
@@ -600,7 +600,7 @@ async def handle_queen_messages(request: web.Request) -> web.Response:
if part_file.suffix != ".json":
continue
try:
part = json.loads(part_file.read_text())
part = json.loads(part_file.read_text(encoding="utf-8"))
part["_node_id"] = node_dir.name
all_messages.append(part)
except (json.JSONDecodeError, OSError):
+2 -2
View File
@@ -277,13 +277,13 @@ class SessionManager:
if not state_path.exists():
continue
try:
state = json.loads(state_path.read_text())
state = json.loads(state_path.read_text(encoding="utf-8"))
if state.get("status") != "active":
continue
state["status"] = "cancelled"
state.setdefault("result", {})["error"] = "Stale session: runtime restarted"
state.setdefault("timestamps", {})["updated_at"] = datetime.now().isoformat()
state_path.write_text(json.dumps(state, indent=2))
state_path.write_text(json.dumps(state, indent=2), encoding="utf-8")
logger.info(
"Marked stale session '%s' as cancelled for agent '%s'", d.name, agent_path.name
)
+2 -2
View File
@@ -95,7 +95,7 @@ class CheckpointStore:
return None
try:
return Checkpoint.model_validate_json(checkpoint_path.read_text())
return Checkpoint.model_validate_json(checkpoint_path.read_text(encoding="utf-8"))
except Exception as e:
logger.error(f"Failed to load checkpoint {checkpoint_id}: {e}")
return None
@@ -123,7 +123,7 @@ class CheckpointStore:
return None
try:
return CheckpointIndex.model_validate_json(self.index_path.read_text())
return CheckpointIndex.model_validate_json(self.index_path.read_text(encoding="utf-8"))
except Exception as e:
logger.error(f"Failed to load checkpoint index: {e}")
return None
+2 -2
View File
@@ -114,7 +114,7 @@ class SessionStore:
if not state_path.exists():
return None
return SessionState.model_validate_json(state_path.read_text())
return SessionState.model_validate_json(state_path.read_text(encoding="utf-8"))
return await asyncio.to_thread(_read)
@@ -151,7 +151,7 @@ class SessionStore:
continue
try:
state = SessionState.model_validate_json(state_path.read_text())
state = SessionState.model_validate_json(state_path.read_text(encoding="utf-8"))
# Apply filters
if status and state.status != status:
+2 -2
View File
@@ -190,7 +190,7 @@ def cmd_test_debug(args: argparse.Namespace) -> int:
# Find which file contains the test
test_file = None
for py_file in tests_dir.glob("test_*.py"):
content = py_file.read_text()
content = py_file.read_text(encoding="utf-8")
if f"def {test_name}" in content or f"async def {test_name}" in content:
test_file = py_file
break
@@ -238,7 +238,7 @@ def _scan_test_files(tests_dir: Path) -> list[dict]:
for test_file in sorted(tests_dir.glob("test_*.py")):
try:
content = test_file.read_text()
content = test_file.read_text(encoding="utf-8")
tree = ast.parse(content)
for node in ast.walk(tree):
+4 -4
View File
@@ -53,7 +53,7 @@ def _get_last_active(agent_name: str) -> str | None:
if not state_file.exists():
continue
try:
data = json.loads(state_file.read_text())
data = json.loads(state_file.read_text(encoding="utf-8"))
ts = data.get("timestamps", {}).get("updated_at")
if ts and (latest is None or ts > latest):
latest = ts
@@ -84,7 +84,7 @@ def _extract_agent_stats(agent_path: Path) -> tuple[int, int, list[str]]:
agent_py = agent_path / "agent.py"
if agent_py.exists():
try:
tree = ast.parse(agent_py.read_text())
tree = ast.parse(agent_py.read_text(encoding="utf-8"))
for node in ast.walk(tree):
# Find `nodes = [...]` assignment
if isinstance(node, ast.Assign):
@@ -99,7 +99,7 @@ def _extract_agent_stats(agent_path: Path) -> tuple[int, int, list[str]]:
agent_json = agent_path / "agent.json"
if agent_json.exists():
try:
data = json.loads(agent_json.read_text())
data = json.loads(agent_json.read_text(encoding="utf-8"))
json_nodes = data.get("nodes", [])
if node_count == 0:
node_count = len(json_nodes)
@@ -150,7 +150,7 @@ def discover_agents() -> dict[str, list[AgentEntry]]:
agent_json = path / "agent.json"
if agent_json.exists():
try:
data = json.loads(agent_json.read_text())
data = json.loads(agent_json.read_text(encoding="utf-8"))
meta = data.get("agent", {})
name = meta.get("name", name)
desc = meta.get("description", desc)
-101
View File
@@ -182,80 +182,6 @@ function NewTabPopover({ open, onClose, anchorRef, discoverAgents, onFromScratch
);
}
// --- LoadAgentPopover ---
interface LoadAgentPopoverProps {
open: boolean;
onClose: () => void;
anchorRef: React.RefObject<HTMLButtonElement | null>;
discoverAgents: DiscoverEntry[];
onSelect: (agentPath: string) => void;
}
function LoadAgentPopover({ open, onClose, anchorRef, discoverAgents, onSelect }: LoadAgentPopoverProps) {
const [pos, setPos] = useState<{ top: number; right: number } | null>(null);
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
if (open && anchorRef.current) {
const rect = anchorRef.current.getBoundingClientRect();
setPos({ top: rect.bottom + 4, right: window.innerWidth - rect.right });
}
}, [open, anchorRef]);
useEffect(() => {
if (!open) return;
const handler = (e: MouseEvent) => {
if (
ref.current && !ref.current.contains(e.target as Node) &&
anchorRef.current && !anchorRef.current.contains(e.target as Node)
) onClose();
};
document.addEventListener("mousedown", handler);
return () => document.removeEventListener("mousedown", handler);
}, [open, onClose, anchorRef]);
useEffect(() => {
if (!open) return;
const handler = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); };
document.addEventListener("keydown", handler);
return () => document.removeEventListener("keydown", handler);
}, [open, onClose]);
if (!open || !pos) return null;
return ReactDOM.createPortal(
<div
ref={ref}
style={{ position: "fixed", top: pos.top, right: pos.right, zIndex: 9999 }}
className="w-60 rounded-xl border border-border/60 bg-card shadow-xl shadow-black/30 overflow-hidden"
>
<div className="flex items-center gap-2 px-3 py-2.5 border-b border-border/40">
<span className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">
Load Agent
</span>
</div>
<div className="p-1.5 flex flex-col max-h-64 overflow-y-auto">
{discoverAgents.map(agent => (
<button
key={agent.path}
onClick={() => { onSelect(agent.path); onClose(); }}
className="flex items-center gap-2.5 w-full px-3 py-2 rounded-lg text-left transition-colors hover:bg-muted/60 text-foreground"
>
<div className="w-6 h-6 rounded-md bg-muted/80 flex items-center justify-center flex-shrink-0">
<Bot className="w-3.5 h-3.5 text-muted-foreground" />
</div>
<span className="text-sm font-medium">{agent.name}</span>
</button>
))}
{discoverAgents.length === 0 && (
<p className="text-xs text-muted-foreground px-3 py-2">No agents found</p>
)}
</div>
</div>,
document.body
);
}
function fmtLogTs(ts: string): string {
try {
const d = new Date(ts);
@@ -402,9 +328,6 @@ export default function Workspace() {
const [selectedNode, setSelectedNode] = useState<GraphNode | null>(null);
const [newTabOpen, setNewTabOpen] = useState(false);
const newTabBtnRef = useRef<HTMLButtonElement>(null);
const [loadAgentOpen, setLoadAgentOpen] = useState(false);
const [loadingWorker, setLoadingWorker] = useState(false);
const loadAgentBtnRef = useRef<HTMLButtonElement>(null);
// Ref mirror of sessionsByAgent so SSE callback can read current graph
// state without adding sessionsByAgent to its dependency array.
@@ -1645,30 +1568,6 @@ export default function Workspace() {
</>
}
>
{activeWorker === "new-agent" && activeAgentState?.ready && !activeAgentState?.graphId && (
<>
<button
ref={loadAgentBtnRef}
onClick={() => setLoadAgentOpen(o => !o)}
disabled={loadingWorker}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium text-muted-foreground hover:text-foreground hover:bg-muted/50 transition-colors flex-shrink-0"
>
{loadingWorker ? (
<Loader2 className="w-3.5 h-3.5 animate-spin" />
) : (
<Bot className="w-3.5 h-3.5" />
)}
Load Agent
</button>
<LoadAgentPopover
open={loadAgentOpen}
onClose={() => setLoadAgentOpen(false)}
anchorRef={loadAgentBtnRef}
discoverAgents={discoverAgents}
onSelect={handleLoadAgent}
/>
</>
)}
<button
onClick={() => setCredentialsOpen(true)}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium text-muted-foreground hover:text-foreground hover:bg-muted/50 transition-colors flex-shrink-0"
+6 -15
View File
@@ -307,8 +307,9 @@ Write-Host ""
Write-Step -Number "1" -Text "Step 1: Checking Python..."
# On Windows "python3.x" aliases don't exist; prefer "python" then "python3"
$PythonCmd = $null
foreach ($candidate in @("python3.13", "python3.12", "python3.11", "python3", "python")) {
foreach ($candidate in @("python", "python3", "python3.13", "python3.12", "python3.11")) {
try {
$ver = & $candidate -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')" 2>$null
if ($LASTEXITCODE -eq 0 -and $ver) {
@@ -326,17 +327,7 @@ foreach ($candidate in @("python3.13", "python3.12", "python3.11", "python3", "p
}
if (-not $PythonCmd) {
# Try plain "python" as final fallback (common on Windows)
try {
$ver = & python -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')" 2>$null
if ($LASTEXITCODE -eq 0) {
Write-Color -Text "Python $ver found but 3.11+ is required." -Color Red
} else {
Write-Color -Text "Python is not installed." -Color Red
}
} catch {
Write-Color -Text "Python is not installed." -Color Red
}
Write-Color -Text "Python 3.11+ is not installed or not on PATH." -Color Red
Write-Host ""
Write-Host "Please install Python 3.11+ from https://python.org"
Write-Host " - Make sure to check 'Add Python to PATH' during installation"
@@ -673,7 +664,7 @@ $imports = @(
$modulesToCheck = @("framework", "aden_tools", "litellm", "framework.mcp.agent_builder_server")
try {
$checkOutput = & uv run $PythonCmd scripts/check_requirements.py @modulesToCheck 2>&1 | Out-String
$checkOutput = & uv run python scripts/check_requirements.py @modulesToCheck 2>&1 | Out-String
$resultJson = $null
# Try to parse JSON result
@@ -1241,7 +1232,7 @@ $verifyErrors = 0
$verifyModules = @("framework", "aden_tools")
try {
$verifyOutput = & uv run $PythonCmd scripts/check_requirements.py @verifyModules 2>&1 | Out-String
$verifyOutput = & uv run python scripts/check_requirements.py @verifyModules 2>&1 | Out-String
$verifyJson = $null
try {
@@ -1251,7 +1242,7 @@ try {
# Fall back to basic checks if JSON parsing fails
foreach ($mod in $verifyModules) {
Write-Host " $([char]0x2B21) $mod... " -NoNewline
$null = & uv run $PythonCmd -c "import $mod" 2>&1
$null = & uv run python -c "import $mod" 2>&1
if ($LASTEXITCODE -eq 0) { Write-Ok "ok" }
else { Write-Fail "failed"; $verifyErrors++ }
}