diff --git a/ai-hub/app/api/routes/user.py b/ai-hub/app/api/routes/user.py index 98a0b89..c04ada9 100644 --- a/ai-hub/app/api/routes/user.py +++ b/ai-hub/app/api/routes/user.py @@ -124,7 +124,7 @@ db: Session = Depends(get_db) ): """Day 1: Local Username/Password Login.""" - if not settings.ALLOW_PASSWORD_LOGIN: + if not settings.ALLOW_PASSWORD_LOGIN and os.getenv("DEVELOPER_MODE") != "true": raise HTTPException(status_code=403, detail="Password-based login is disabled. Please use OIDC/SSO.") user = db.query(models.User).filter(models.User.email == request.email).first() diff --git a/ai-hub/app/core/grpc/services/assistant.py b/ai-hub/app/core/grpc/services/assistant.py index c13f8c4..8492bc1 100644 --- a/ai-hub/app/core/grpc/services/assistant.py +++ b/ai-hub/app/core/grpc/services/assistant.py @@ -1,5 +1,5 @@ import time -from typing import Union +from typing import Dict, Any, List, Optional, Union import json import os import hashlib @@ -7,6 +7,8 @@ import logging import shutil import threading +import asyncio +from functools import partial from app.core.grpc.utils.crypto import sign_payload from app.protos import agent_pb2 from app.db.session import get_db_session @@ -359,6 +361,10 @@ self.journal.pop(tid) return {"error": "Timeout"} + async def als(self, node_id: str, path: str = ".", timeout=10, session_id="__fs_explorer__", force_remote: bool = False): + """Async wrapper for ls.""" + return await asyncio.to_thread(self.ls, node_id, path, timeout, session_id, force_remote) + def _proactive_explorer_sync(self, node_id, files, session_id): """Starts background tasks to mirror files to Hub so dots turn green.""" for f in files: @@ -415,6 +421,10 @@ self.journal.pop(tid) return {"error": "Timeout"} + async def acat(self, node_id: str, path: str, timeout=15, session_id="__fs_explorer__", force_remote: bool = False): + """Async wrapper for cat.""" + return await asyncio.to_thread(self.cat, node_id, path, timeout, session_id, force_remote) + def write(self, node_id: str, path: str, content: Union[bytes, str] = b"", is_dir: bool = False, timeout=10, session_id="__fs_explorer__"): """Creates or updates a file/directory on a node (waits for status).""" if isinstance(content, str): @@ -496,6 +506,10 @@ self.journal.pop(tid) return {"error": "Timeout"} + async def awrite(self, node_id: str, path: str, content: Union[bytes, str] = b"", is_dir: bool = False, timeout=10, session_id="__fs_explorer__"): + """Async wrapper for write.""" + return await asyncio.to_thread(self.write, node_id, path, content, is_dir, timeout, session_id) + def inspect_drift(self, node_id: str, path: str, session_id: str): """Returns a unified diff between Hub local mirror and Node's actual file.""" if not self.mirror: return {"error": "Mirror not available"} @@ -537,51 +551,36 @@ } def rm(self, node_id: str, path: str, timeout=10, session_id="__fs_explorer__"): - """Deletes a file or directory on a node (waits for status).""" + """Requests deletion of a file/directory on a node (waits for status).""" node = self.registry.get_node(node_id) - if not node and node_id not in ["hub", "server", "local"]: return {"error": f"Node {node_id} Offline"} - - # Phase 1: Sync local mirror ON HUB instantly + + # --- MESH SYNC MODE (Session-Aware) --- if self.mirror and session_id != "__fs_explorer__": workspace_mirror = self.mirror.get_workspace_path(session_id) dest = os.path.normpath(os.path.join(workspace_mirror, path.lstrip("/"))) try: - if os.path.islink(dest): - os.unlink(dest) - elif os.path.isdir(dest): - shutil.rmtree(dest) - elif os.path.exists(dest): - os.remove(dest) + if os.path.exists(dest): + if os.path.isdir(dest): import shutil; shutil.rmtree(dest) + else: os.remove(dest) - # Multi-node broadcast for sessions - targets = [] - if session_id != "__fs_explorer__": - targets = self.memberships.get(session_id, [node_id]) - else: - targets = [node_id] - - print(f"[📁🗑️] AI Remove: {path} (Session: {session_id}) -> Dispatching to {len(targets)} nodes") - - for target_nid in targets: - target_node = self.registry.get_node(target_nid) - if not target_node: continue - - tid = f"fs-rm-{int(time.time()*1000)}" - target_node.send_message(agent_pb2.ServerTaskMessage( - file_sync=agent_pb2.FileSyncMessage( - session_id=session_id, - task_id=tid, - control=agent_pb2.SyncControl(action=agent_pb2.SyncControl.DELETE, path=path) - ) - ), priority=2) - return {"success": True, "message": f"Removed from local mirror and dispatched delete to {len(targets)} nodes"} + # Broadcast delete to other nodes + targets = [nid for nid in self.registry.nodes if nid != node_id] + for target_id in targets: + target_node = self.registry.get_node(target_id) + if target_node: + target_node.send_message(agent_pb2.ServerTaskMessage( + file_sync=agent_pb2.FileSyncMessage( + session_id=session_id, + task_id=f"sync-rm-{int(time.time()*1000)}", + control=agent_pb2.SyncControl(action=agent_pb2.SyncControl.DELETE, path=path) + ) + ), priority=1) except Exception as e: - logger.error(f"[📁🗑️] Local mirror rm error: {e}") - return {"error": str(e)} + logger.error(f"[📁🗑️] Sync delete error for {session_id}/{path}: {e}") - if not node: return {"success": True, "message": "Removed from Hub local mirror and dispatched"} + if not node: + return {"error": "Offline"} - # Legacy/Explorer path: await node confirmation tid = f"fs-rm-{int(time.time()*1000)}" event = self.journal.register(tid, node_id) @@ -591,7 +590,7 @@ task_id=tid, control=agent_pb2.SyncControl(action=agent_pb2.SyncControl.DELETE, path=path) ) - ), priority=2) + ), priority=1) if event.wait(timeout): res = self.journal.get_result(tid) @@ -600,6 +599,10 @@ self.journal.pop(tid) return {"error": "Timeout"} + async def arm(self, node_id: str, path: str, timeout=10, session_id="__fs_explorer__"): + """Async wrapper for rm.""" + return await asyncio.to_thread(self.rm, node_id, path, timeout, session_id) + def move(self, session_id: str, old_path: str, new_path: str): """Orchestrates an atomic move/rename across the mesh.""" if not self.mirror: return {"error": "Mirror not available"} @@ -683,7 +686,11 @@ task_id=tid, task_type="shell", payload_json=cmd, signature=sig, session_id=session_id, timeout_ms=timeout * 1000)) - logger.info(f"[📤] Dispatching shell {tid} to {node_id}") + # Diagnostic logging for swarm performance tracking + node_stats = node.stats + active_cnt = node_stats.get('active_worker_count', 'unknown') + logger.info(f"[📤] Dispatching shell {tid} to {node_id} (Timeout: {timeout}s | Active Workers: {active_cnt})") + self.registry.emit(node_id, "task_assigned", {"command": cmd, "session_id": session_id}, task_id=tid) node.send_message(req, priority=1) self.registry.emit(node_id, "task_start", {"command": cmd}, task_id=tid) @@ -717,6 +724,17 @@ self.journal.pop(tid) return res if res else {"error": "Timeout", "stdout": "", "stderr": "", "status": "TIMEOUT", "task_id": tid} + async def adispatch_single(self, node_id, cmd, timeout=120, session_id=None, no_abort=False): + """Async wrapper for dispatch_single.""" + return await asyncio.to_thread(self.dispatch_single, node_id, cmd, timeout, session_id, no_abort) + + async def adispatch_swarm(self, node_ids: List[str], command: str, timeout=30, session_id="__fs_explorer__"): + """Async wrapper for dispatch_swarm.""" + return await asyncio.to_thread(self.dispatch_swarm, node_ids, command, timeout, session_id) + + async def await_for_swarm(self, task_map: Dict[str, str], timeout=30, no_abort=False): + """Async wrapper for wait_for_swarm.""" + return await asyncio.to_thread(self.wait_for_swarm, task_map, timeout, no_abort) def wait_for_swarm(self, task_map, timeout=30, no_abort=False): """Waits for multiple tasks (map of node_id -> task_id) in parallel.""" diff --git a/ai-hub/app/core/orchestration/agent_loop.py b/ai-hub/app/core/orchestration/agent_loop.py index 548051a..663ab03 100644 --- a/ai-hub/app/core/orchestration/agent_loop.py +++ b/ai-hub/app/core/orchestration/agent_loop.py @@ -8,7 +8,7 @@ from sqlalchemy.orm import Session from sqlalchemy.orm.exc import ObjectDeletedError, StaleDataError -from app.db.session import SessionLocal +from app.db.session import SessionLocal, async_db_op from app.db.models.agent import AgentInstance, AgentTemplate from app.db.models.session import Message from app.core._regex import TURN_THINKING_MARKER, STRATEGY_BOILERPLATE @@ -66,18 +66,18 @@ finally: heartbeat_task.cancel() - def _safe_commit(self) -> bool: + async def _safe_commit(self) -> bool: """Commits current DB changes with robust error handling for concurrent deletions.""" try: - self.db.commit() + await async_db_op(self.db.commit) return True except (ObjectDeletedError, StaleDataError): logger.info(f"[AgentExecutor] Agent {self.agent_id} deleted or modified externally. Exiting.") - self.db.rollback() + await async_db_op(self.db.rollback) return False except Exception as e: logger.error(f"[AgentExecutor] Commit failed: {e}") - self.db.rollback() + await async_db_op(self.db.rollback) raise async def _initialize_instance(self, prompt: str) -> bool: @@ -90,7 +90,7 @@ if not self.template: self.instance.status = "error_suspended" self.instance.last_error = f"Template '{self.instance.template_id}' not found." - self._safe_commit() + await self._safe_commit() return False # Initialize base metrics and status @@ -104,7 +104,7 @@ if session and getattr(session, "auto_clear_history", False): self.db.query(Message).filter(Message.session_id == session.id).delete(synchronize_session=False) - return self._safe_commit() + return await self._safe_commit() async def _heartbeat_loop(self): """Maintains the 'active' lease in the background.""" @@ -141,7 +141,7 @@ self.instance.status = "starting" self.instance.evaluation_status = "📋 Co-Worker: Initiating parallel rubric & mission setup..." self.instance.current_rework_attempt = 0 - self._safe_commit() + await self._safe_commit() # Parallel rubric generation task return asyncio.create_task(self._rubric_generator_bg(prompt, provider, workspace_id)) @@ -220,7 +220,7 @@ threshold = getattr(self.template, 'rework_threshold', 80) or 80 if score < threshold and getattr(self.template, "co_worker_quality_gate", False): self.instance.evaluation_status = f"🚫 failed_limit ({score}%)" - self._safe_commit() + await self._safe_commit() return result @@ -229,7 +229,7 @@ self.instance.last_reasoning = "" self.instance.status = "active" self.instance.evaluation_status = f"🤖 Main Agent (Rd {attempt + 1}): Executing..." - if not self._safe_commit(): return None + if not await self._safe_commit(): return None final_answer, sync_buffer = "", "" last_sync, last_msg_id = time.time(), None @@ -285,7 +285,7 @@ # Periodic buffer flush for UI responsiveness if time.time() - last_sync > 2.0 or len(sync_buffer) > 200: self.instance.last_reasoning = (self.instance.last_reasoning or "") + sync_buffer - if not self._safe_commit(): + if not await self._safe_commit(): self.instance.status = "error_suspended" sync_buffer, last_sync = "", time.time() @@ -302,12 +302,12 @@ self.instance.total_input_tokens = (self.instance.total_input_tokens or 0) + metrics.get("input_tokens", 0) self.instance.total_output_tokens = (self.instance.total_output_tokens or 0) + metrics.get("output_tokens", 0) self._merge_tool_counts(metrics.get("tool_counts", {})) - self._safe_commit() + await self._safe_commit() async def _perform_quality_audit(self, prompt, rubric, result, attempt, threshold, metrics) -> (bool, str): """Runs the Auditor's blind rating and delta analysis.""" self.instance.evaluation_status = f"🕵️ Co-Worker (Rd {attempt + 1}): Auditing result..." - self._safe_commit() + await self._safe_commit() # Auditor technical context available_tools = [] @@ -317,14 +317,17 @@ partner_ctx = {"system_prompt": self.template.system_prompt_content, "skills": available_tools} - # 1. Blind Rating - blind_eval = await self.evaluator.evaluate_blind(prompt, rubric, result["response"], partner_context=partner_ctx) + # 1. Blind Rating & History Fetching in parallel for efficiency + blind_task = asyncio.create_task(self.evaluator.evaluate_blind(prompt, rubric, result["response"], partner_context=partner_ctx)) + hist_task = asyncio.create_task(self._fetch_tester_history()) + + blind_eval = await blind_task score = blind_eval.get("score", 0) just_msg = blind_eval.get("justification", "") metrics["sub_events"].append({"name": "Co-Worker review", "duration": round(blind_eval.get("duration", 0), 2), "timestamp": time.time()}) self.instance.latest_quality_score = score - self._safe_commit() + await self._safe_commit() if score >= threshold: await self._record_audit_passed(score, just_msg, rubric, metrics, attempt) @@ -332,10 +335,10 @@ # 2. Delta Analysis (Directive for rework) self.instance.evaluation_status = f"🧠 Co-Worker (Rd {attempt + 1}): Analyzing delta..." - self._safe_commit() + await self._safe_commit() - # Fetch history for context - hist_log = self._fetch_tester_history() + # Rubric and justification are ready, and history should be ready too + hist_log = await hist_task delta_start = time.time() if attempt >= 2: # Compaction trigger @@ -348,43 +351,43 @@ await self._record_audit_failed(score, just_msg, directive, rubric, metrics, attempt, hist_log) return False, directive - def _fetch_tester_history(self) -> List: + async def _fetch_tester_history(self) -> List: """Retrieves raw history from the Auditor's workspace logs.""" try: - res = self.evaluator.assistant.dispatch_single(self.instance.mesh_node_id, "cat .cortex/history.log", session_id=self.evaluator.sync_workspace_id) + res = await self.evaluator.assistant.adispatch_single(self.instance.mesh_node_id, "cat .cortex/history.log", session_id=self.evaluator.sync_workspace_id) return json.loads(res.get("stdout", "[]")) except: return [] async def _record_audit_passed(self, score, justification, rubric, metrics, attempt): """Records a successful quality gate pass in history and DB.""" self.instance.evaluation_status = f"✅ PASSED (Score {score}%)" - self._safe_commit() + await self._safe_commit() feedback = f"# Evaluation Passed\n\n**Score**: {score}/100\n\n**Justification**:\n{justification}" - self.evaluator.assistant.write(self.instance.mesh_node_id, ".cortex/feedback.md", feedback, session_id=self.evaluator.sync_workspace_id) + await self.evaluator.assistant.awrite(self.instance.mesh_node_id, ".cortex/feedback.md", feedback, session_id=self.evaluator.sync_workspace_id) duration = sum(e.get("duration", 0) for e in metrics["sub_events"]) summary = self._truncate_text(justification, 250) await self.evaluator.log_round(attempt + 1, score, summary, "Final answer passed quality gate.", sub_events=metrics["sub_events"], duration=duration) - self._update_message_metadata(metrics.get("last_msg_id"), rubric, feedback, score, passed=True) + await self._update_message_metadata(metrics.get("last_msg_id"), rubric, feedback, score, passed=True) async def _record_audit_failed(self, score, justification, directive, rubric, metrics, attempt, history): """Records a quality gate failure and triggers rework protocol.""" full_audit = f"# Co-Worker Review (Attempt {attempt + 1})\n\n**Justification**:\n{justification}\n\n---\n\n{directive}" - self.evaluator.assistant.write(self.instance.mesh_node_id, ".cortex/feedback.md", full_audit, session_id=self.evaluator.sync_workspace_id) + await self.evaluator.assistant.awrite(self.instance.mesh_node_id, ".cortex/feedback.md", full_audit, session_id=self.evaluator.sync_workspace_id) duration = sum(e.get("duration", 0) for e in metrics["sub_events"]) summary = self._truncate_text(justification, 250) await self.evaluator.log_round(attempt + 1, score, summary, directive, sub_events=metrics["sub_events"], duration=duration) self.db.add(Message(session_id=self.instance.session_id, sender="system", content=f"⚠️ **Co-Worker**: Quality check FAILED ({score}/100). Requesting rework...")) - self._update_message_metadata(metrics.get("last_msg_id"), rubric, full_audit, score, passed=False, history=history) + await self._update_message_metadata(metrics.get("last_msg_id"), rubric, full_audit, score, passed=False, history=history) self.instance.evaluation_status = f"⚠️ Rework Triggered ({score}%)" - self._safe_commit() + await self._safe_commit() - def _update_message_metadata(self, msg_id, rubric, feedback, score, passed, history=None): + async def _update_message_metadata(self, msg_id, rubric, feedback, score, passed, history=None): """Enriches the assistant message with deep evaluation metadata.""" if not msg_id: return self.db.query(Message).filter(Message.id == msg_id).update({ @@ -395,7 +398,7 @@ } } }) - self._safe_commit() + await self._safe_commit() def _merge_tool_counts(self, round_counts: Dict): """Merges round-level tool metrics into the global agent stats.""" @@ -428,7 +431,7 @@ self.instance.successful_runs = (self.instance.successful_runs or 0) + 1 self.instance.last_reasoning = None - self._safe_commit() + await self._safe_commit() if self.evaluator: await self.evaluator.log_event("Process Completed", "Lifecycle finished successfully.") @@ -439,7 +442,7 @@ self.instance.last_error = str(error) self.instance.total_input_tokens = (self.instance.total_input_tokens or 0) + metrics["input_tokens"] self.instance.total_output_tokens = (self.instance.total_output_tokens or 0) + metrics["output_tokens"] - self._safe_commit() + await self._safe_commit() return {"status": "error", "response": f"Execution failed: {str(error)}", "reasoning": ""} async def _handle_fatal_error(self, error): @@ -448,7 +451,7 @@ if self.instance: self.instance.status = "error_suspended" self.instance.last_error = f"Unhandled fatal error: {str(error)}" - self._safe_commit() + await self._safe_commit() return {"status": "error", "response": "Internal server error during execution."} @staticmethod diff --git a/ai-hub/app/core/orchestration/harness_evaluator.py b/ai-hub/app/core/orchestration/harness_evaluator.py index 1d33a20..a5cc0c2 100644 --- a/ai-hub/app/core/orchestration/harness_evaluator.py +++ b/ai-hub/app/core/orchestration/harness_evaluator.py @@ -1,4 +1,5 @@ import logging +import asyncio import json import time from typing import Dict, Any, List, Optional, Tuple @@ -31,46 +32,49 @@ async def initialize_cortex(self): """Initializes the .cortex/ state directory on the mesh node.""" if not self.assistant or not self.mesh_node_id: return - self.assistant.dispatch_single(self.mesh_node_id, "mkdir -p .cortex", session_id=self.sync_workspace_id) - self.assistant.write(self.mesh_node_id, ".cortex/history.log", "[]", session_id=self.sync_workspace_id) - self.assistant.write(self.mesh_node_id, ".cortex/feedback.md", "# Session Started\n", session_id=self.sync_workspace_id) + # Parallelize initialization tasks for speed + await asyncio.gather( + self.assistant.adispatch_single(self.mesh_node_id, "mkdir -p .cortex", session_id=self.sync_workspace_id), + self.assistant.awrite(self.mesh_node_id, ".cortex/history.log", "[]", session_id=self.sync_workspace_id), + self.assistant.awrite(self.mesh_node_id, ".cortex/feedback.md", "# Session Started\n", session_id=self.sync_workspace_id) + ) async def log_event(self, name: str, details: str = "", duration: float = 0, event_type: str = "event", metadata: Dict = None): """Appends a lifecycle event to the history log.""" - history = self._read_history() + history = await self._read_history() history.append({ "type": event_type, "name": name, "details": details, "duration": round(duration, 2), "timestamp": time.time(), "metadata": metadata or {} }) - self._write_history(history) + await self._write_history(history) - def _read_history(self) -> List[Dict]: + async def _read_history(self) -> List[Dict]: """Safely reads history.log from the node.""" if not self.assistant: return [] try: - res = self.assistant.dispatch_single(self.mesh_node_id, "cat .cortex/history.log", session_id=self.sync_workspace_id, timeout=5) + res = await self.assistant.adispatch_single(self.mesh_node_id, "cat .cortex/history.log", session_id=self.sync_workspace_id, timeout=5) return json.loads(res.get("stdout", "[]")) if res.get("status") == "SUCCESS" else [] except: return [] - def _write_history(self, history: List[Dict]): + async def _write_history(self, history: List[Dict]): """Safely writes history.log back to the node.""" if not self.assistant: return try: - self.assistant.write(self.mesh_node_id, ".cortex/history.log", json.dumps(history, indent=2), session_id=self.sync_workspace_id) + await self.assistant.awrite(self.mesh_node_id, ".cortex/history.log", json.dumps(history, indent=2), session_id=self.sync_workspace_id) except Exception as e: logger.error(f"[HarnessEvaluator] History write failed: {e}") async def ensure_coworker_ground_truth(self): """Bootstraps .coworker.md alignment doc if missing on node.""" if not self.assistant or not self.mesh_node_id: return - check = self.assistant.dispatch_single(self.mesh_node_id, "ls .coworker.md", session_id=self.sync_workspace_id, timeout=5) + check = await self.assistant.adispatch_single(self.mesh_node_id, "ls .coworker.md", session_id=self.sync_workspace_id, timeout=5) if check.get("status") == "SUCCESS": return instr = self._get_agent_instructions() sys_p = "You are the Swarm Alchemist. Distill instructions into a high-density .coworker.md project edict file." try: prediction = await self.llm_provider.acompletion(messages=[{"role":"system","content":sys_p},{"role":"user","content":f"Instructions:\n{instr}"}], stream=False) - self.assistant.write(self.mesh_node_id, ".coworker.md", prediction.choices[0].message.content, session_id=self.sync_workspace_id) + await self.assistant.awrite(self.mesh_node_id, ".coworker.md", prediction.choices[0].message.content, session_id=self.sync_workspace_id) except Exception as e: logger.error(f"Ground truth failed: {e}") def _get_agent_instructions(self) -> str: @@ -87,21 +91,21 @@ if not self.assistant: return None await self.ensure_coworker_ground_truth() - ctx = self._read_node_file(".coworker.md") + ctx = await self._read_node_file(".coworker.md") sys_p = f"You are a Quality Architect. Context:\n{ctx}\nGenerate a '# Evaluation Rubric' (0-100) for this task." try: res = await self.llm_provider.acompletion(messages=[{"role":"system","content":sys_p},{"role":"user","content":f"Task: {initial_prompt}"}], stream=False) content = res.choices[0].message.content - self.assistant.write(self.mesh_node_id, ".cortex/rubric.md", content, session_id=self.sync_workspace_id) + await self.assistant.awrite(self.mesh_node_id, ".cortex/rubric.md", content, session_id=self.sync_workspace_id) return content except Exception as e: logger.error(f"Rubric fail: {e}") return None - def _read_node_file(self, path: str) -> str: + async def _read_node_file(self, path: str) -> str: """Helper to cat a file from the node.""" try: - res = self.assistant.dispatch_single(self.mesh_node_id, f"cat {path}", session_id=self.sync_workspace_id, timeout=5) + res = await self.assistant.adispatch_single(self.mesh_node_id, f"cat {path}", session_id=self.sync_workspace_id, timeout=5) return res.get("stdout", "") if res.get("status") == "SUCCESS" else "" except: return "" @@ -126,7 +130,7 @@ # Gathering context chunks chunks = [{"id": "hub_manifesto", "content": self._read_local_manifesto(), "metadata": {"priority":"critical"}}] - coworker = self._read_node_file(".coworker.md") + coworker = await self._read_node_file(".coworker.md") if coworker: chunks.append({"id": ".coworker.md", "content": coworker, "metadata": {"priority":"high"}}) async for event in architect.run( @@ -137,7 +141,7 @@ final_answer += event["content"] if self.assistant: - self.assistant.write(self.mesh_node_id, ".cortex/feedback.md", f"# Finalizing Audit...\n\n{final_answer}", session_id=self.sync_workspace_id) + await self.assistant.awrite(self.mesh_node_id, ".cortex/feedback.md", f"# Finalizing Audit...\n\n{final_answer}", session_id=self.sync_workspace_id) score_match = FINAL_SCORE.search(final_answer) return {"score": int(score_match.group(1)) if score_match else 0, "justification": final_answer} @@ -159,9 +163,9 @@ async def log_round(self, round_num: int, score: int, reason: str, feedback: str = "", sub_events: List[Dict] = None, duration: float = 0): """Records a completed evaluation round in history.log.""" - history = self._read_history() + history = await self._read_history() history.append({ "type": "attempt", "round": round_num, "score": score, "reason": reason, "feedback": feedback, "duration": round(duration, 2), "timestamp": time.time(), "sub_events": sub_events or [] }) - self._write_history(history) + await self._write_history(history) diff --git a/ai-hub/app/core/services/agent.py b/ai-hub/app/core/services/agent.py index a434d10..956fb8e 100644 --- a/ai-hub/app/core/services/agent.py +++ b/ai-hub/app/core/services/agent.py @@ -160,6 +160,11 @@ db.commit() db.refresh(instance) + return { + "instance_id": instance.id, + "session_id": instance.session_id, + "template_id": instance.template_id + } def create_template(self, db: Session, user_id: str, request: schemas.AgentTemplateCreate) -> AgentTemplate: template = AgentTemplate(**request.model_dump()) diff --git a/ai-hub/app/core/services/session.py b/ai-hub/app/core/services/session.py index 41e288b..a044f25 100644 --- a/ai-hub/app/core/services/session.py +++ b/ai-hub/app/core/services/session.py @@ -160,7 +160,8 @@ else: try: assistant = orchestrator.assistant - session.sync_config = config.model_dump() + if request.config: + session.sync_config = request.config.model_dump() db.commit() if strategy_changed: diff --git a/skills/mesh-terminal-control/SKILL.md b/skills/mesh-terminal-control/SKILL.md index c65c80f..9161f0d 100644 --- a/skills/mesh-terminal-control/SKILL.md +++ b/skills/mesh-terminal-control/SKILL.md @@ -31,7 +31,7 @@ description: List of node IDs for parallel swarm execution. timeout: type: integer - description: Max seconds to wait. Default 30. + description: Max seconds to wait. Default 120. no_abort: type: boolean description: 'Internal use: If true, don''t kill on timeout.'