diff --git a/ai-hub/app/core/grpc/services/assistant.py b/ai-hub/app/core/grpc/services/assistant.py index fa56993..923ae10 100644 --- a/ai-hub/app/core/grpc/services/assistant.py +++ b/ai-hub/app/core/grpc/services/assistant.py @@ -420,9 +420,10 @@ if isinstance(content, str): content = content.encode("utf-8") 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 (Zero Latency) + + # --- MESH SYNC MODE (Session-Aware) --- + # If we have a session, we prioritize writing to the local mirror even if the target node is offline. + # The node will reconcile its state from the Hub mirror upon reconnection. 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("/"))) @@ -466,7 +467,10 @@ logger.error(f"[📁✏️] Local mirror write error: {e}") return {"error": str(e)} - if not node: return {"success": True, "message": "Written to Hub local mirror and dispatched"} + if not node: + if node_id in ["hub", "server", "local"]: + return {"success": True, "message": "Operation completed on Hub local node"} + return {"error": f"Node {node_id} Offline"} # Legacy/Explorer path: await node confirmation tid = f"fs-write-{int(time.time()*1000)}" diff --git a/ai-hub/app/core/orchestration/agent_loop.py b/ai-hub/app/core/orchestration/agent_loop.py index 7777d24..3877e50 100644 --- a/ai-hub/app/core/orchestration/agent_loop.py +++ b/ai-hub/app/core/orchestration/agent_loop.py @@ -15,9 +15,26 @@ """Asynchronous execution loop for the agent.""" # Create a fresh DB session for the background task db: Session = SessionLocal() + + def safe_commit(): + """Helper to commit and handle session errors or deleted objects gracefully.""" + try: + db.commit() + return True + except Exception as e: + db.rollback() + from sqlalchemy.orm.exc import ObjectDeletedError, StaleDataError + if isinstance(e, (ObjectDeletedError, StaleDataError)): + print(f"[AgentExecutor] Agent {agent_id} was deleted or modified externally. Exiting loop.") + return False + print(f"[AgentExecutor] Commit failed for {agent_id}: {e}") + # Re-raise if it's something truly unexpected + raise + try: instance = db.query(AgentInstance).filter(AgentInstance.id == agent_id).first() if not instance or not prompt: + db.close() return # Acquire Lease @@ -25,7 +42,7 @@ instance.status = "active" instance.last_error = None instance.total_runs = (instance.total_runs or 0) + 1 - db.commit() + if not safe_commit(): return # Launch secondary heartbeat task @@ -50,7 +67,7 @@ if not template: instance.status = "error_suspended" instance.last_error = f"Template '{instance.template_id}' not found." - db.commit() + if not safe_commit(): return return # Configuration for Rework Loop @@ -115,7 +132,7 @@ "evaluation_status": "📋 Co-Worker: Generating request-specific rubric.md...", "current_rework_attempt": 0 }) - db.commit() + if not safe_commit(): return # Emit status if registry exists registry = getattr(services.rag_service, "node_registry_service", None) @@ -144,7 +161,7 @@ if session_id: if getattr(agent_session, "auto_clear_history", False): db.query(Message).filter(Message.session_id == session_id).delete(synchronize_session=False) - db.commit() + if not safe_commit(): return current_prompt = prompt current_attempt = 0 @@ -168,7 +185,7 @@ instance = db.query(AgentInstance).filter(AgentInstance.id == agent_id).first() instance.last_reasoning = "" instance.evaluation_status = f"🤖 Main Agent (Rd {current_attempt + 1}): Executing..." - db.commit() + if not safe_commit(): return execution_start = time.time() @@ -211,7 +228,7 @@ content_buffer = "" last_db_sync_time = now sync_token_count = 0 - db.commit() + if not safe_commit(): return if registry and instance.mesh_node_id: registry.emit(instance.mesh_node_id, "reasoning", { @@ -224,7 +241,7 @@ if content_buffer: instance = db.query(AgentInstance).filter(AgentInstance.id == agent_id).first() instance.last_reasoning = (instance.last_reasoning or "") + content_buffer - db.commit() + if not safe_commit(): return content_buffer = "" exec_duration = time.time() - execution_start @@ -243,12 +260,12 @@ # --- EVALUATION PHASE (Co-Worker Loop) --- if evaluator and final_answer: instance.evaluation_status = "evaluating" - db.commit() + if not safe_commit(): return # Stage 2A: Blind Rating instance = db.query(AgentInstance).filter(AgentInstance.id == agent_id).first() instance.evaluation_status = f"🕵️ Co-Worker (Rd {current_attempt + 1}): Auditing result against criteria..." - db.commit() + if not safe_commit(): return if registry and instance.mesh_node_id: registry.emit(instance.mesh_node_id, "status_update", {"evaluation_status": instance.evaluation_status}) @@ -261,12 +278,12 @@ # Update instance with latest score db.query(AgentInstance).filter(AgentInstance.id == agent_id).update({"latest_quality_score": score}) - db.commit() + if not safe_commit(): return # Check Threshold if score >= rework_threshold: instance.evaluation_status = f"✅ PASSED (Score {score}%)" - db.commit() + if not safe_commit(): return if registry and instance.mesh_node_id: registry.emit(instance.mesh_node_id, "status_update", {"evaluation_status": instance.evaluation_status}) @@ -287,7 +304,7 @@ if current_attempt >= max_rework_attempts: instance.evaluation_status = "failed_limit" instance.last_error = f"Co-Worker Gate: Quality fell below {rework_threshold}% after {max_rework_attempts} attempts." - db.commit() + if not safe_commit(): return # M3: Aggregate total failure duration and truncated summary for timeline total_fail_duration = sum(e.get("duration", 0) for e in round_sub_events) @@ -300,7 +317,7 @@ # Stage Delta (Gap Analysis) instance = db.query(AgentInstance).filter(AgentInstance.id == agent_id).first() instance.evaluation_status = f"🧠 Co-Worker (Rd {current_attempt + 1}): Analyzing reasoning delta..." - db.commit() + if not safe_commit(): return if registry and instance.mesh_node_id: registry.emit(instance.mesh_node_id, "status_update", {"evaluation_status": instance.evaluation_status}) @@ -340,7 +357,7 @@ db.add(Message(session_id=session_id, sender="system", content=f"⚠️ **Co-Worker**: Quality check FAILED (Score: {score}/100). Requesting rework...")) db.query(AgentInstance).filter(AgentInstance.id == agent_id).update({"evaluation_status": f"⚠️ Rework Triggered ({score}%)"}) - db.commit() + if not safe_commit(): return continue # Start next loop iteration else: break # No co-worker or no answer @@ -353,7 +370,7 @@ if instance: instance.status = "error_suspended" instance.last_error = str(e) - db.commit() + if not safe_commit(): return return { "status": "error", "response": f"Execution failed: {str(e)}", @@ -390,7 +407,7 @@ # Clear reasoning as the task is now complete instance.last_reasoning = None - db.commit() + if not safe_commit(): return return final_result @@ -402,7 +419,7 @@ if instance: instance.status = "error_suspended" instance.last_error = f"Unhandled loop error: {str(e)}" - db.commit() + if not safe_commit(): return return { "status": "error", "response": "Internal server error during execution.", diff --git a/ai-hub/app/core/orchestration/harness_evaluator.py b/ai-hub/app/core/orchestration/harness_evaluator.py index 41b28b3..5c71266 100644 --- a/ai-hub/app/core/orchestration/harness_evaluator.py +++ b/ai-hub/app/core/orchestration/harness_evaluator.py @@ -90,9 +90,100 @@ except Exception as e: logger.error(f"[HarnessEvaluator] Event logging failed: {e}") + async def ensure_coworker_ground_truth(self): + """ + SWARM ALIGNMENT: Ensures .coworker.md exists on the node. + If missing, it distills the main agent's instructions into a ground-truth document. + """ + if not self.assistant or not self.mesh_node_id: return + + # 1. Existence Check + check = self.assistant.dispatch_single( + self.mesh_node_id, + "ls .coworker.md", + session_id=self.sync_workspace_id, + timeout=5 + ) + if check.get("status") == "SUCCESS": + return # Already exists + + logger.info(f"[HarnessEvaluator] .coworker.md missing on {self.mesh_node_id}. Generating ground truth...") + + # 2. Source Material Discovery + source_instruction = "" + try: + instance = self.db.query(AgentInstance).filter(AgentInstance.id == self.agent_id).first() + if instance and instance.template: + # Priority: session override -> template path + if instance.session and instance.session.system_prompt_override: + source_instruction = instance.session.system_prompt_override + else: + # If it's a file path, we'd need to read it. If it's the raw text, use it. + # For now, we use a fallback if it looks like a short slug. + raw = instance.template.system_prompt_path or "" + if len(raw) > 100: + source_instruction = raw + else: + from app.core.orchestration.profiles import DEFAULT_PROMPT_TEMPLATE + source_instruction = DEFAULT_PROMPT_TEMPLATE + except Exception as e: + logger.warning(f"[HarnessEvaluator] Failed to fetch agent instructions for ground truth: {e}") + from app.core.orchestration.profiles import DEFAULT_PROMPT_TEMPLATE + source_instruction = DEFAULT_PROMPT_TEMPLATE + + # 3. AI Distillation + system_prompt = """You are the Swarm Alchemist. +Your goal is to distill the provided "Agent System Instructions" into a concise, high-density ".coworker.md" file. +This file acts as the "Ground Truth" for other agents working in the same swarm. + +It MUST include: +1. **Core Edicts**: The non-negotiable rules of this project. +2. **Architecture**: Key technical constraints (e.g., pathing, tool usage, sync folders). +3. **Alignment**: How a 'perfectly aligned' implementation should look. + +Keep it under 1000 tokens. Format as Markdown.""" + + user_prompt = f"Agent Instructions:\n{source_instruction}\n\nGenerate the .coworker.md Aligned Knowledge Base now." + + try: + messages = [ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": user_prompt} + ] + prediction = await self.llm_provider.acompletion(messages=messages, stream=False) + ground_truth = prediction.choices[0].message.content + + # 4. Persistence + self.assistant.write( + self.mesh_node_id, + ".coworker.md", + ground_truth, + session_id=self.sync_workspace_id + ) + logger.info(f"[HarnessEvaluator] Successfully bootstrapped .coworker.md for {self.agent_id}") + await self.log_event("Ground Truth Sync", ".coworker.md bootstrapped from Agent Template.") + except Exception as e: + logger.error(f"[HarnessEvaluator] Ground truth generation failed: {e}") + async def generate_rubric(self, initial_prompt: str): """Stage 1: Pre-Execution. Generate a task-specific rubric.md.""" if not self.assistant: return None + + # CLEANUP: Purge old rubric from the node to ensure a fresh start for the new request + try: + self.assistant.dispatch_single( + self.mesh_node_id, + "rm .cortex/rubric.md", + session_id=self.sync_workspace_id, + timeout=5 + ) + logger.debug(f"[HarnessEvaluator] Purged stale rubric on {self.mesh_node_id}") + except: + pass + + # BOOTSTRAP: Ensure Ground Truth / .coworker.md exists before generating rubric + await self.ensure_coworker_ground_truth() + start = time.time() # --- File-Based Knowledge Discovery (Aligned with ClaudeCode) --- @@ -107,7 +198,7 @@ if cmd_res.get("status") == "SUCCESS": coworker_context = f"\n\nPROJECT-SPECIFIC CONTEXT (from .coworker.md):\n{cmd_res.get('stdout', '')}" except: - logger.debug("[HarnessEvaluator] No .coworker.md found; proceeding with generic rubric.") + pass # Silently continue if cat fails system_prompt = f"""You are a Quality Control Architect for a live infrastructure swarm. Your task is to analyze a user request and generate a specific Evaluation Rubric in Markdown. diff --git a/ai-hub/app/core/orchestration/memory.py b/ai-hub/app/core/orchestration/memory.py index f8f301f..34685df 100644 --- a/ai-hub/app/core/orchestration/memory.py +++ b/ai-hub/app/core/orchestration/memory.py @@ -138,8 +138,19 @@ return [system_msg, history_msg, ack_msg] + recent_msg - def _default_context_postprocessor(self, contexts: List[str]) -> str: - return "\n\n".join(contexts) or "No context provided." + def _default_context_postprocessor(self, contexts: List[Any]) -> str: + """Safe context joining that handles both raw strings and enriched dictionaries.""" + processed = [] + for c in contexts: + if isinstance(c, str): + processed.append(c) + elif isinstance(c, dict): + # Try common keys for content + content = c.get("content") or c.get("text") or str(c) + processed.append(content) + else: + processed.append(str(c)) + return "\n\n".join(processed) or "No context provided." def _default_history_formatter(self, history: List[models.Message]) -> str: """High-performance history formatting using string methods instead of regex.""" diff --git a/ai-hub/app/core/services/browser_client.py b/ai-hub/app/core/services/browser_client.py index d1b913d..f83921d 100644 --- a/ai-hub/app/core/services/browser_client.py +++ b/ai-hub/app/core/services/browser_client.py @@ -239,21 +239,27 @@ max_concurrent=max_concurrent, extract_markdown=extract_markdown ) - resp = await self.stub.ParallelFetch(req, timeout=120) + # Now iterating over a stream + stream = self.stub.ParallelFetch(req, timeout=120) results = [] - for r in resp.results: - results.append({ - "url": r.url, - "title": r.title, - "content": r.content_markdown, - "success": r.success, - "error": r.error, - "fetch_mode": getattr(r, 'fetch_mode', '') - }) + + async for resp in stream: + for r in resp.results: + status_icon = "✅" if r.success else "❌" + await self._report_status(f"{status_icon} Research complete for: `{r.url}`", on_event) + results.append({ + "url": r.url, + "title": r.title, + "content": r.content_markdown, + "success": r.success, + "error": r.error, + "fetch_mode": getattr(r, 'fetch_mode', '') + }) return {"success": True, "results": results} except Exception as e: + await self._report_status(f"❌ Parallel fetch encountered a critical fault: {e}", on_event) return {"success": False, "error": str(e)} # Alias for AI compatibility diff --git a/ai-hub/app/core/services/rag.py b/ai-hub/app/core/services/rag.py index 771f2ae..57ab633 100644 --- a/ai-hub/app/core/services/rag.py +++ b/ai-hub/app/core/services/rag.py @@ -172,8 +172,17 @@ chunks = [] total_len = 0 for chunk in reversed(list(live.terminal_history)[-40:]): - chunks.insert(0, chunk) - total_len += len(chunk) + # Defensive handle for non-string chunks (e.g. dicts from some serialization) + chunk_str = "" + if isinstance(chunk, str): + chunk_str = chunk + elif isinstance(chunk, dict): + chunk_str = chunk.get("output") or chunk.get("content") or str(chunk) + else: + chunk_str = str(chunk) + + chunks.insert(0, chunk_str) + total_len += len(chunk_str) if total_len > 4000: break history_blob = "".join(chunks) diff --git a/ai-hub/app/protos/browser.proto b/ai-hub/app/protos/browser.proto index fcf7944..ad1a55c 100644 --- a/ai-hub/app/protos/browser.proto +++ b/ai-hub/app/protos/browser.proto @@ -11,7 +11,7 @@ rpc Evaluate(EvalRequest) returns (BrowserResponse); rpc GetSnapshot(SnapshotRequest) returns (BrowserResponse); rpc CloseSession(CloseRequest) returns (CloseResponse); - rpc ParallelFetch(ParallelFetchRequest) returns (ParallelFetchResponse); + rpc ParallelFetch(ParallelFetchRequest) returns (stream ParallelFetchResponse); } message NavigateRequest { diff --git a/ai-hub/app/protos/browser_pb2.py b/ai-hub/app/protos/browser_pb2.py index 64c55f0..cabe7ab 100644 --- a/ai-hub/app/protos/browser_pb2.py +++ b/ai-hub/app/protos/browser_pb2.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! -# source: protos/browser.proto +# source: browser.proto # Protobuf Python Version: 4.25.1 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor @@ -14,39 +14,39 @@ -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x14protos/browser.proto\x12\x07\x62rowser\"K\n\x0fNavigateRequest\x12\x0b\n\x03url\x18\x01 \x01(\t\x12\x12\n\nsession_id\x18\x02 \x01(\t\x12\x17\n\x0fwait_until_idle\x18\x03 \x01(\x08\"J\n\x0c\x43lickRequest\x12\x10\n\x08selector\x18\x01 \x01(\t\x12\x12\n\nsession_id\x18\x02 \x01(\t\x12\t\n\x01x\x18\x03 \x01(\x05\x12\t\n\x01y\x18\x04 \x01(\x05\"V\n\x0bTypeRequest\x12\x10\n\x08selector\x18\x01 \x01(\t\x12\x0c\n\x04text\x18\x02 \x01(\t\x12\x12\n\nsession_id\x18\x03 \x01(\t\x12\x13\n\x0bpress_enter\x18\x04 \x01(\x08\"4\n\x0cHoverRequest\x12\x10\n\x08selector\x18\x01 \x01(\t\x12\x12\n\nsession_id\x18\x02 \x01(\t\"W\n\rScrollRequest\x12\x12\n\nsession_id\x18\x01 \x01(\t\x12\x0f\n\x07\x64\x65lta_x\x18\x02 \x01(\x05\x12\x0f\n\x07\x64\x65lta_y\x18\x03 \x01(\x05\x12\x10\n\x08selector\x18\x04 \x01(\t\"1\n\x0b\x45valRequest\x12\x0e\n\x06script\x18\x01 \x01(\t\x12\x12\n\nsession_id\x18\x02 \x01(\t\"l\n\x0fSnapshotRequest\x12\x12\n\nsession_id\x18\x01 \x01(\t\x12\x1a\n\x12include_screenshot\x18\x02 \x01(\x08\x12\x13\n\x0binclude_dom\x18\x03 \x01(\x08\x12\x14\n\x0cinclude_a11y\x18\x04 \x01(\x08\"\"\n\x0c\x43loseRequest\x12\x12\n\nsession_id\x18\x01 \x01(\t\" \n\rCloseResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\"j\n\x14ParallelFetchRequest\x12\x0c\n\x04urls\x18\x01 \x03(\t\x12\x12\n\nsession_id\x18\x02 \x01(\t\x12\x16\n\x0emax_concurrent\x18\x03 \x01(\x05\x12\x18\n\x10\x65xtract_markdown\x18\x04 \x01(\x08\"\xcd\x01\n\x15ParallelFetchResponse\x12;\n\x07results\x18\x01 \x03(\x0b\x32*.browser.ParallelFetchResponse.FetchResult\x1aw\n\x0b\x46\x65tchResult\x12\x0b\n\x03url\x18\x01 \x01(\t\x12\r\n\x05title\x18\x02 \x01(\t\x12\x18\n\x10\x63ontent_markdown\x18\x03 \x01(\t\x12\x0f\n\x07success\x18\x04 \x01(\x08\x12\r\n\x05\x65rror\x18\x05 \x01(\t\x12\x12\n\nfetch_mode\x18\x06 \x01(\t\"\xbb\x01\n\x0f\x42rowserResponse\x12\x12\n\nsession_id\x18\x01 \x01(\t\x12\x0b\n\x03url\x18\x02 \x01(\t\x12\r\n\x05title\x18\x03 \x01(\t\x12\x0e\n\x06status\x18\x04 \x01(\t\x12\x15\n\rerror_message\x18\x05 \x01(\t\x12\x10\n\x08\x64om_path\x18\x06 \x01(\t\x12\x17\n\x0fscreenshot_path\x18\x07 \x01(\t\x12\x11\n\ta11y_path\x18\x08 \x01(\t\x12\x13\n\x0b\x65val_result\x18\t \x01(\t2\xc6\x04\n\x0e\x42rowserService\x12>\n\x08Navigate\x12\x18.browser.NavigateRequest\x1a\x18.browser.BrowserResponse\x12\x38\n\x05\x43lick\x12\x15.browser.ClickRequest\x1a\x18.browser.BrowserResponse\x12\x36\n\x04Type\x12\x14.browser.TypeRequest\x1a\x18.browser.BrowserResponse\x12\x38\n\x05Hover\x12\x15.browser.HoverRequest\x1a\x18.browser.BrowserResponse\x12:\n\x06Scroll\x12\x16.browser.ScrollRequest\x1a\x18.browser.BrowserResponse\x12:\n\x08\x45valuate\x12\x14.browser.EvalRequest\x1a\x18.browser.BrowserResponse\x12\x41\n\x0bGetSnapshot\x12\x18.browser.SnapshotRequest\x1a\x18.browser.BrowserResponse\x12=\n\x0c\x43loseSession\x12\x15.browser.CloseRequest\x1a\x16.browser.CloseResponse\x12N\n\rParallelFetch\x12\x1d.browser.ParallelFetchRequest\x1a\x1e.browser.ParallelFetchResponseb\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\rbrowser.proto\x12\x07\x62rowser\"K\n\x0fNavigateRequest\x12\x0b\n\x03url\x18\x01 \x01(\t\x12\x12\n\nsession_id\x18\x02 \x01(\t\x12\x17\n\x0fwait_until_idle\x18\x03 \x01(\x08\"J\n\x0c\x43lickRequest\x12\x10\n\x08selector\x18\x01 \x01(\t\x12\x12\n\nsession_id\x18\x02 \x01(\t\x12\t\n\x01x\x18\x03 \x01(\x05\x12\t\n\x01y\x18\x04 \x01(\x05\"V\n\x0bTypeRequest\x12\x10\n\x08selector\x18\x01 \x01(\t\x12\x0c\n\x04text\x18\x02 \x01(\t\x12\x12\n\nsession_id\x18\x03 \x01(\t\x12\x13\n\x0bpress_enter\x18\x04 \x01(\x08\"4\n\x0cHoverRequest\x12\x10\n\x08selector\x18\x01 \x01(\t\x12\x12\n\nsession_id\x18\x02 \x01(\t\"W\n\rScrollRequest\x12\x12\n\nsession_id\x18\x01 \x01(\t\x12\x0f\n\x07\x64\x65lta_x\x18\x02 \x01(\x05\x12\x0f\n\x07\x64\x65lta_y\x18\x03 \x01(\x05\x12\x10\n\x08selector\x18\x04 \x01(\t\"1\n\x0b\x45valRequest\x12\x0e\n\x06script\x18\x01 \x01(\t\x12\x12\n\nsession_id\x18\x02 \x01(\t\"l\n\x0fSnapshotRequest\x12\x12\n\nsession_id\x18\x01 \x01(\t\x12\x1a\n\x12include_screenshot\x18\x02 \x01(\x08\x12\x13\n\x0binclude_dom\x18\x03 \x01(\x08\x12\x14\n\x0cinclude_a11y\x18\x04 \x01(\x08\"\"\n\x0c\x43loseRequest\x12\x12\n\nsession_id\x18\x01 \x01(\t\" \n\rCloseResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\"j\n\x14ParallelFetchRequest\x12\x0c\n\x04urls\x18\x01 \x03(\t\x12\x12\n\nsession_id\x18\x02 \x01(\t\x12\x16\n\x0emax_concurrent\x18\x03 \x01(\x05\x12\x18\n\x10\x65xtract_markdown\x18\x04 \x01(\x08\"\xb9\x01\n\x15ParallelFetchResponse\x12;\n\x07results\x18\x01 \x03(\x0b\x32*.browser.ParallelFetchResponse.FetchResult\x1a\x63\n\x0b\x46\x65tchResult\x12\x0b\n\x03url\x18\x01 \x01(\t\x12\r\n\x05title\x18\x02 \x01(\t\x12\x18\n\x10\x63ontent_markdown\x18\x03 \x01(\t\x12\x0f\n\x07success\x18\x04 \x01(\x08\x12\r\n\x05\x65rror\x18\x05 \x01(\t\"\xbb\x01\n\x0f\x42rowserResponse\x12\x12\n\nsession_id\x18\x01 \x01(\t\x12\x0b\n\x03url\x18\x02 \x01(\t\x12\r\n\x05title\x18\x03 \x01(\t\x12\x0e\n\x06status\x18\x04 \x01(\t\x12\x15\n\rerror_message\x18\x05 \x01(\t\x12\x10\n\x08\x64om_path\x18\x06 \x01(\t\x12\x17\n\x0fscreenshot_path\x18\x07 \x01(\t\x12\x11\n\ta11y_path\x18\x08 \x01(\t\x12\x13\n\x0b\x65val_result\x18\t \x01(\t2\xc8\x04\n\x0e\x42rowserService\x12>\n\x08Navigate\x12\x18.browser.NavigateRequest\x1a\x18.browser.BrowserResponse\x12\x38\n\x05\x43lick\x12\x15.browser.ClickRequest\x1a\x18.browser.BrowserResponse\x12\x36\n\x04Type\x12\x14.browser.TypeRequest\x1a\x18.browser.BrowserResponse\x12\x38\n\x05Hover\x12\x15.browser.HoverRequest\x1a\x18.browser.BrowserResponse\x12:\n\x06Scroll\x12\x16.browser.ScrollRequest\x1a\x18.browser.BrowserResponse\x12:\n\x08\x45valuate\x12\x14.browser.EvalRequest\x1a\x18.browser.BrowserResponse\x12\x41\n\x0bGetSnapshot\x12\x18.browser.SnapshotRequest\x1a\x18.browser.BrowserResponse\x12=\n\x0c\x43loseSession\x12\x15.browser.CloseRequest\x1a\x16.browser.CloseResponse\x12P\n\rParallelFetch\x12\x1d.browser.ParallelFetchRequest\x1a\x1e.browser.ParallelFetchResponse0\x01\x62\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'protos.browser_pb2', _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'browser_pb2', _globals) if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None - _globals['_NAVIGATEREQUEST']._serialized_start=33 - _globals['_NAVIGATEREQUEST']._serialized_end=108 - _globals['_CLICKREQUEST']._serialized_start=110 - _globals['_CLICKREQUEST']._serialized_end=184 - _globals['_TYPEREQUEST']._serialized_start=186 - _globals['_TYPEREQUEST']._serialized_end=272 - _globals['_HOVERREQUEST']._serialized_start=274 - _globals['_HOVERREQUEST']._serialized_end=326 - _globals['_SCROLLREQUEST']._serialized_start=328 - _globals['_SCROLLREQUEST']._serialized_end=415 - _globals['_EVALREQUEST']._serialized_start=417 - _globals['_EVALREQUEST']._serialized_end=466 - _globals['_SNAPSHOTREQUEST']._serialized_start=468 - _globals['_SNAPSHOTREQUEST']._serialized_end=576 - _globals['_CLOSEREQUEST']._serialized_start=578 - _globals['_CLOSEREQUEST']._serialized_end=612 - _globals['_CLOSERESPONSE']._serialized_start=614 - _globals['_CLOSERESPONSE']._serialized_end=646 - _globals['_PARALLELFETCHREQUEST']._serialized_start=648 - _globals['_PARALLELFETCHREQUEST']._serialized_end=754 - _globals['_PARALLELFETCHRESPONSE']._serialized_start=757 - _globals['_PARALLELFETCHRESPONSE']._serialized_end=962 - _globals['_PARALLELFETCHRESPONSE_FETCHRESULT']._serialized_start=843 - _globals['_PARALLELFETCHRESPONSE_FETCHRESULT']._serialized_end=962 - _globals['_BROWSERRESPONSE']._serialized_start=965 - _globals['_BROWSERRESPONSE']._serialized_end=1152 - _globals['_BROWSERSERVICE']._serialized_start=1155 - _globals['_BROWSERSERVICE']._serialized_end=1737 + _globals['_NAVIGATEREQUEST']._serialized_start=26 + _globals['_NAVIGATEREQUEST']._serialized_end=101 + _globals['_CLICKREQUEST']._serialized_start=103 + _globals['_CLICKREQUEST']._serialized_end=177 + _globals['_TYPEREQUEST']._serialized_start=179 + _globals['_TYPEREQUEST']._serialized_end=265 + _globals['_HOVERREQUEST']._serialized_start=267 + _globals['_HOVERREQUEST']._serialized_end=319 + _globals['_SCROLLREQUEST']._serialized_start=321 + _globals['_SCROLLREQUEST']._serialized_end=408 + _globals['_EVALREQUEST']._serialized_start=410 + _globals['_EVALREQUEST']._serialized_end=459 + _globals['_SNAPSHOTREQUEST']._serialized_start=461 + _globals['_SNAPSHOTREQUEST']._serialized_end=569 + _globals['_CLOSEREQUEST']._serialized_start=571 + _globals['_CLOSEREQUEST']._serialized_end=605 + _globals['_CLOSERESPONSE']._serialized_start=607 + _globals['_CLOSERESPONSE']._serialized_end=639 + _globals['_PARALLELFETCHREQUEST']._serialized_start=641 + _globals['_PARALLELFETCHREQUEST']._serialized_end=747 + _globals['_PARALLELFETCHRESPONSE']._serialized_start=750 + _globals['_PARALLELFETCHRESPONSE']._serialized_end=935 + _globals['_PARALLELFETCHRESPONSE_FETCHRESULT']._serialized_start=836 + _globals['_PARALLELFETCHRESPONSE_FETCHRESULT']._serialized_end=935 + _globals['_BROWSERRESPONSE']._serialized_start=938 + _globals['_BROWSERRESPONSE']._serialized_end=1125 + _globals['_BROWSERSERVICE']._serialized_start=1128 + _globals['_BROWSERSERVICE']._serialized_end=1712 # @@protoc_insertion_point(module_scope) diff --git a/ai-hub/app/protos/browser_pb2_grpc.py b/ai-hub/app/protos/browser_pb2_grpc.py index 276488e..0ca5947 100644 --- a/ai-hub/app/protos/browser_pb2_grpc.py +++ b/ai-hub/app/protos/browser_pb2_grpc.py @@ -2,7 +2,7 @@ """Client and server classes corresponding to protobuf-defined services.""" import grpc -from app.protos import browser_pb2 as protos_dot_browser__pb2 +import browser_pb2 as browser__pb2 class BrowserServiceStub(object): @@ -16,48 +16,48 @@ """ self.Navigate = channel.unary_unary( '/browser.BrowserService/Navigate', - request_serializer=protos_dot_browser__pb2.NavigateRequest.SerializeToString, - response_deserializer=protos_dot_browser__pb2.BrowserResponse.FromString, + request_serializer=browser__pb2.NavigateRequest.SerializeToString, + response_deserializer=browser__pb2.BrowserResponse.FromString, ) self.Click = channel.unary_unary( '/browser.BrowserService/Click', - request_serializer=protos_dot_browser__pb2.ClickRequest.SerializeToString, - response_deserializer=protos_dot_browser__pb2.BrowserResponse.FromString, + request_serializer=browser__pb2.ClickRequest.SerializeToString, + response_deserializer=browser__pb2.BrowserResponse.FromString, ) self.Type = channel.unary_unary( '/browser.BrowserService/Type', - request_serializer=protos_dot_browser__pb2.TypeRequest.SerializeToString, - response_deserializer=protos_dot_browser__pb2.BrowserResponse.FromString, + request_serializer=browser__pb2.TypeRequest.SerializeToString, + response_deserializer=browser__pb2.BrowserResponse.FromString, ) self.Hover = channel.unary_unary( '/browser.BrowserService/Hover', - request_serializer=protos_dot_browser__pb2.HoverRequest.SerializeToString, - response_deserializer=protos_dot_browser__pb2.BrowserResponse.FromString, + request_serializer=browser__pb2.HoverRequest.SerializeToString, + response_deserializer=browser__pb2.BrowserResponse.FromString, ) self.Scroll = channel.unary_unary( '/browser.BrowserService/Scroll', - request_serializer=protos_dot_browser__pb2.ScrollRequest.SerializeToString, - response_deserializer=protos_dot_browser__pb2.BrowserResponse.FromString, + request_serializer=browser__pb2.ScrollRequest.SerializeToString, + response_deserializer=browser__pb2.BrowserResponse.FromString, ) self.Evaluate = channel.unary_unary( '/browser.BrowserService/Evaluate', - request_serializer=protos_dot_browser__pb2.EvalRequest.SerializeToString, - response_deserializer=protos_dot_browser__pb2.BrowserResponse.FromString, + request_serializer=browser__pb2.EvalRequest.SerializeToString, + response_deserializer=browser__pb2.BrowserResponse.FromString, ) self.GetSnapshot = channel.unary_unary( '/browser.BrowserService/GetSnapshot', - request_serializer=protos_dot_browser__pb2.SnapshotRequest.SerializeToString, - response_deserializer=protos_dot_browser__pb2.BrowserResponse.FromString, + request_serializer=browser__pb2.SnapshotRequest.SerializeToString, + response_deserializer=browser__pb2.BrowserResponse.FromString, ) self.CloseSession = channel.unary_unary( '/browser.BrowserService/CloseSession', - request_serializer=protos_dot_browser__pb2.CloseRequest.SerializeToString, - response_deserializer=protos_dot_browser__pb2.CloseResponse.FromString, + request_serializer=browser__pb2.CloseRequest.SerializeToString, + response_deserializer=browser__pb2.CloseResponse.FromString, ) - self.ParallelFetch = channel.unary_unary( + self.ParallelFetch = channel.unary_stream( '/browser.BrowserService/ParallelFetch', - request_serializer=protos_dot_browser__pb2.ParallelFetchRequest.SerializeToString, - response_deserializer=protos_dot_browser__pb2.ParallelFetchResponse.FromString, + request_serializer=browser__pb2.ParallelFetchRequest.SerializeToString, + response_deserializer=browser__pb2.ParallelFetchResponse.FromString, ) @@ -123,48 +123,48 @@ rpc_method_handlers = { 'Navigate': grpc.unary_unary_rpc_method_handler( servicer.Navigate, - request_deserializer=protos_dot_browser__pb2.NavigateRequest.FromString, - response_serializer=protos_dot_browser__pb2.BrowserResponse.SerializeToString, + request_deserializer=browser__pb2.NavigateRequest.FromString, + response_serializer=browser__pb2.BrowserResponse.SerializeToString, ), 'Click': grpc.unary_unary_rpc_method_handler( servicer.Click, - request_deserializer=protos_dot_browser__pb2.ClickRequest.FromString, - response_serializer=protos_dot_browser__pb2.BrowserResponse.SerializeToString, + request_deserializer=browser__pb2.ClickRequest.FromString, + response_serializer=browser__pb2.BrowserResponse.SerializeToString, ), 'Type': grpc.unary_unary_rpc_method_handler( servicer.Type, - request_deserializer=protos_dot_browser__pb2.TypeRequest.FromString, - response_serializer=protos_dot_browser__pb2.BrowserResponse.SerializeToString, + request_deserializer=browser__pb2.TypeRequest.FromString, + response_serializer=browser__pb2.BrowserResponse.SerializeToString, ), 'Hover': grpc.unary_unary_rpc_method_handler( servicer.Hover, - request_deserializer=protos_dot_browser__pb2.HoverRequest.FromString, - response_serializer=protos_dot_browser__pb2.BrowserResponse.SerializeToString, + request_deserializer=browser__pb2.HoverRequest.FromString, + response_serializer=browser__pb2.BrowserResponse.SerializeToString, ), 'Scroll': grpc.unary_unary_rpc_method_handler( servicer.Scroll, - request_deserializer=protos_dot_browser__pb2.ScrollRequest.FromString, - response_serializer=protos_dot_browser__pb2.BrowserResponse.SerializeToString, + request_deserializer=browser__pb2.ScrollRequest.FromString, + response_serializer=browser__pb2.BrowserResponse.SerializeToString, ), 'Evaluate': grpc.unary_unary_rpc_method_handler( servicer.Evaluate, - request_deserializer=protos_dot_browser__pb2.EvalRequest.FromString, - response_serializer=protos_dot_browser__pb2.BrowserResponse.SerializeToString, + request_deserializer=browser__pb2.EvalRequest.FromString, + response_serializer=browser__pb2.BrowserResponse.SerializeToString, ), 'GetSnapshot': grpc.unary_unary_rpc_method_handler( servicer.GetSnapshot, - request_deserializer=protos_dot_browser__pb2.SnapshotRequest.FromString, - response_serializer=protos_dot_browser__pb2.BrowserResponse.SerializeToString, + request_deserializer=browser__pb2.SnapshotRequest.FromString, + response_serializer=browser__pb2.BrowserResponse.SerializeToString, ), 'CloseSession': grpc.unary_unary_rpc_method_handler( servicer.CloseSession, - request_deserializer=protos_dot_browser__pb2.CloseRequest.FromString, - response_serializer=protos_dot_browser__pb2.CloseResponse.SerializeToString, + request_deserializer=browser__pb2.CloseRequest.FromString, + response_serializer=browser__pb2.CloseResponse.SerializeToString, ), - 'ParallelFetch': grpc.unary_unary_rpc_method_handler( + 'ParallelFetch': grpc.unary_stream_rpc_method_handler( servicer.ParallelFetch, - request_deserializer=protos_dot_browser__pb2.ParallelFetchRequest.FromString, - response_serializer=protos_dot_browser__pb2.ParallelFetchResponse.SerializeToString, + request_deserializer=browser__pb2.ParallelFetchRequest.FromString, + response_serializer=browser__pb2.ParallelFetchResponse.SerializeToString, ), } generic_handler = grpc.method_handlers_generic_handler( @@ -188,8 +188,8 @@ timeout=None, metadata=None): return grpc.experimental.unary_unary(request, target, '/browser.BrowserService/Navigate', - protos_dot_browser__pb2.NavigateRequest.SerializeToString, - protos_dot_browser__pb2.BrowserResponse.FromString, + browser__pb2.NavigateRequest.SerializeToString, + browser__pb2.BrowserResponse.FromString, options, channel_credentials, insecure, call_credentials, compression, wait_for_ready, timeout, metadata) @@ -205,8 +205,8 @@ timeout=None, metadata=None): return grpc.experimental.unary_unary(request, target, '/browser.BrowserService/Click', - protos_dot_browser__pb2.ClickRequest.SerializeToString, - protos_dot_browser__pb2.BrowserResponse.FromString, + browser__pb2.ClickRequest.SerializeToString, + browser__pb2.BrowserResponse.FromString, options, channel_credentials, insecure, call_credentials, compression, wait_for_ready, timeout, metadata) @@ -222,8 +222,8 @@ timeout=None, metadata=None): return grpc.experimental.unary_unary(request, target, '/browser.BrowserService/Type', - protos_dot_browser__pb2.TypeRequest.SerializeToString, - protos_dot_browser__pb2.BrowserResponse.FromString, + browser__pb2.TypeRequest.SerializeToString, + browser__pb2.BrowserResponse.FromString, options, channel_credentials, insecure, call_credentials, compression, wait_for_ready, timeout, metadata) @@ -239,8 +239,8 @@ timeout=None, metadata=None): return grpc.experimental.unary_unary(request, target, '/browser.BrowserService/Hover', - protos_dot_browser__pb2.HoverRequest.SerializeToString, - protos_dot_browser__pb2.BrowserResponse.FromString, + browser__pb2.HoverRequest.SerializeToString, + browser__pb2.BrowserResponse.FromString, options, channel_credentials, insecure, call_credentials, compression, wait_for_ready, timeout, metadata) @@ -256,8 +256,8 @@ timeout=None, metadata=None): return grpc.experimental.unary_unary(request, target, '/browser.BrowserService/Scroll', - protos_dot_browser__pb2.ScrollRequest.SerializeToString, - protos_dot_browser__pb2.BrowserResponse.FromString, + browser__pb2.ScrollRequest.SerializeToString, + browser__pb2.BrowserResponse.FromString, options, channel_credentials, insecure, call_credentials, compression, wait_for_ready, timeout, metadata) @@ -273,8 +273,8 @@ timeout=None, metadata=None): return grpc.experimental.unary_unary(request, target, '/browser.BrowserService/Evaluate', - protos_dot_browser__pb2.EvalRequest.SerializeToString, - protos_dot_browser__pb2.BrowserResponse.FromString, + browser__pb2.EvalRequest.SerializeToString, + browser__pb2.BrowserResponse.FromString, options, channel_credentials, insecure, call_credentials, compression, wait_for_ready, timeout, metadata) @@ -290,8 +290,8 @@ timeout=None, metadata=None): return grpc.experimental.unary_unary(request, target, '/browser.BrowserService/GetSnapshot', - protos_dot_browser__pb2.SnapshotRequest.SerializeToString, - protos_dot_browser__pb2.BrowserResponse.FromString, + browser__pb2.SnapshotRequest.SerializeToString, + browser__pb2.BrowserResponse.FromString, options, channel_credentials, insecure, call_credentials, compression, wait_for_ready, timeout, metadata) @@ -307,8 +307,8 @@ timeout=None, metadata=None): return grpc.experimental.unary_unary(request, target, '/browser.BrowserService/CloseSession', - protos_dot_browser__pb2.CloseRequest.SerializeToString, - protos_dot_browser__pb2.CloseResponse.FromString, + browser__pb2.CloseRequest.SerializeToString, + browser__pb2.CloseResponse.FromString, options, channel_credentials, insecure, call_credentials, compression, wait_for_ready, timeout, metadata) @@ -323,8 +323,8 @@ wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary(request, target, '/browser.BrowserService/ParallelFetch', - protos_dot_browser__pb2.ParallelFetchRequest.SerializeToString, - protos_dot_browser__pb2.ParallelFetchResponse.FromString, + return grpc.experimental.unary_stream(request, target, '/browser.BrowserService/ParallelFetch', + browser__pb2.ParallelFetchRequest.SerializeToString, + browser__pb2.ParallelFetchResponse.FromString, options, channel_credentials, insecure, call_credentials, compression, wait_for_ready, timeout, metadata) diff --git a/ai-hub/integration_tests/conftest.py b/ai-hub/integration_tests/conftest.py index 3abaa4c..4398f47 100644 --- a/ai-hub/integration_tests/conftest.py +++ b/ai-hub/integration_tests/conftest.py @@ -22,7 +22,7 @@ 5. Spin up node docker containers with correct tokens. """ print("\n[conftest] Starting Mesh Integration Setup...") - client = httpx.Client(timeout=10.0) + client = httpx.Client(timeout=30.0) # 1. Login print(f"[conftest] Logging in as {ADMIN_EMAIL}...") @@ -117,17 +117,25 @@ group_id = group_r.json().get("id") # Give group access to nodes for node_id in [NODE_1, NODE_2]: - client.post(f"{BASE_URL}/nodes/admin/{node_id}/access", json={ - "group_id": group_id, - "access_level": "use" - }) + client.post( + f"{BASE_URL}/nodes/admin/{node_id}/access", + params={"admin_id": user_id}, + json={ + "group_id": group_id, + "access_level": "use" + } + ) # CRITICAL FIX: Ensure the user's DB preferences points to these fresh # nodes so that tools correctly route instead of using stale nodes from prior runs. updated_prefs = { "default_node_ids": [NODE_1] } - client.patch(f"{BASE_URL}/nodes/preferences", json=updated_prefs) + client.patch( + f"{BASE_URL}/nodes/preferences", + params={"user_id": user_id}, + json=updated_prefs + ) # 5. Start Node Processes is_docker_disabled = os.getenv("SKIP_DOCKER_NODES", "true").lower() == "true" diff --git a/ai-hub/integration_tests/test_advanced_fs.py b/ai-hub/integration_tests/test_advanced_fs.py index ca0eeee..efc92fb 100644 --- a/ai-hub/integration_tests/test_advanced_fs.py +++ b/ai-hub/integration_tests/test_advanced_fs.py @@ -13,7 +13,8 @@ # ── Configuration ────────────────────────────────────────────────────────────── BASE_URL = os.getenv("SYNC_TEST_BASE_URL", "http://127.0.0.1:8002/api/v1/") -USER_ID = os.getenv("SYNC_TEST_USER_ID", "c4401d34-8784-4d6e-93a0-c702bd202b66") +def _user_id(): + return os.getenv("SYNC_TEST_USER_ID", "c4401d34-8784-4d6e-93a0-c702bd202b66") NODE_1 = os.getenv("SYNC_TEST_NODE1", "test-node-1") NODE_2 = os.getenv("SYNC_TEST_NODE2", "test-node-2") @@ -87,7 +88,7 @@ @pytest.fixture(scope="module") def swarm_session(sync_client): - r = sync_client.post(f"{SESSIONS_PATH}/", json={"user_id": USER_ID, "provider_name": "gemini", "feature_name": "swarm_control"}, headers=_headers()) + r = sync_client.post(f"{SESSIONS_PATH}/", json={"user_id": _user_id(), "provider_name": "gemini", "feature_name": "swarm_control"}, headers=_headers()) session_id = r.json()["id"] r2 = sync_client.post(f"{SESSIONS_PATH}/{session_id}/nodes", json={"node_ids": [NODE_1, NODE_2], "config": {"source": "empty"}}, headers=_headers()) workspace = r2.json()["sync_workspace_id"] @@ -102,6 +103,7 @@ def test_mesh_move_atomic(self, sync_client, swarm_session): """move file on hub -> gone from old, present on new across all nodes.""" + print("\n[DEBUG] test_mesh_move_atomic STARTING...") filename = _unique("move_src") destname = _unique("move_dst") content = f"Move Payload {uuid.uuid4()}" diff --git a/ai-hub/integration_tests/test_coworker_flow.py b/ai-hub/integration_tests/test_coworker_flow.py index 1857de9..111e42d 100644 --- a/ai-hub/integration_tests/test_coworker_flow.py +++ b/ai-hub/integration_tests/test_coworker_flow.py @@ -22,16 +22,6 @@ with httpx.Client(timeout=30.0) as client: try: - # 1. Register a test node - node_payload = { - "node_id": node_id, - "display_name": "Co-Worker SC-1 Node", - "is_active": True, - "skill_config": {"shell": {"enabled": True}, "sync": {"enabled": True}} - } - r_node = client.post(f"{BASE_URL}/nodes/admin", params={"admin_id": admin_id}, json=node_payload) - assert r_node.status_code in [200, 409], f"Node registration failed: {r_node.text}" - # 2. Deploy Agent with co_worker_quality_gate=True deploy_payload = { "name": "SC-1 Mirror Agent", @@ -80,7 +70,6 @@ finally: if instance_id: client.delete(f"{BASE_URL}/agents/{instance_id}", headers=_headers()) - client.delete(f"{BASE_URL}/nodes/admin/{node_id}", params={"admin_id": admin_id}) def test_coworker_sc3_limit_check(): """ @@ -96,16 +85,6 @@ with httpx.Client(timeout=30.0) as client: try: - # 1. Register a test node - node_payload = { - "node_id": node_id, - "display_name": "Co-Worker SC-3 Node", - "is_active": True, - "skill_config": {"shell": {"enabled": True}, "sync": {"enabled": True}} - } - r_node = client.post(f"{BASE_URL}/nodes/admin", params={"admin_id": admin_id}, json=node_payload) - assert r_node.status_code in [200, 409], f"Node registration failed: {r_node.text}" - # 2. Deploy Agent with max_rework_attempts=1 and rework_threshold=100 deploy_payload = { "name": "SC-3 Limit Agent", @@ -159,7 +138,6 @@ finally: if instance_id: client.delete(f"{BASE_URL}/agents/{instance_id}", headers=_headers()) - client.delete(f"{BASE_URL}/nodes/admin/{node_id}", params={"admin_id": admin_id}) def test_coworker_sc2_rework_loop(): """ @@ -174,11 +152,7 @@ with httpx.Client(timeout=30.0) as client: try: - r_node = client.post(f"{BASE_URL}/nodes/admin", params={"admin_id": admin_id}, json={ - "node_id": node_id, "display_name": "SC-2 Node", "is_active": True, "skill_config": {"shell": {"enabled": True}} - }) - assert r_node.status_code in [200, 409] - + # 2. Deploy Agent with rework loop deploy_payload = { "name": "SC-2 Rework Agent", "system_prompt": "You are a stubborn tester.", @@ -220,7 +194,6 @@ finally: if instance_id: client.delete(f"{BASE_URL}/agents/{instance_id}", headers=_headers()) - client.delete(f"{BASE_URL}/nodes/admin/{node_id}", params={"admin_id": admin_id}) def test_coworker_sc4_context_compaction(): """ @@ -233,10 +206,7 @@ instance_id = None with httpx.Client(timeout=30.0) as client: try: - # Register node to satisfy 422 - client.post(f"{BASE_URL}/nodes/admin", params={"admin_id": admin_id}, json={ - "node_id": node_id, "display_name": "SC-4 Node", "is_active": True - }) + # Register node check removed (leveraging session-scoped nodes) deploy_payload = { "name": "SC-4 Compaction Agent", @@ -256,5 +226,4 @@ instance_id = r_deploy.json()["instance_id"] finally: if instance_id: client.delete(f"{BASE_URL}/agents/{instance_id}", headers=_headers()) - client.delete(f"{BASE_URL}/nodes/admin/{node_id}", params={"admin_id": admin_id}) diff --git a/ai-hub/integration_tests/test_coworker_full_journey.py b/ai-hub/integration_tests/test_coworker_full_journey.py new file mode 100644 index 0000000..4360dcd --- /dev/null +++ b/ai-hub/integration_tests/test_coworker_full_journey.py @@ -0,0 +1,138 @@ +import pytest +import httpx +import os +import time +import json +from conftest import BASE_URL + +def _headers(): + uid = os.getenv("SYNC_TEST_USER_ID", "9a333ccd-9c3f-432f-a030-7b1e1284a436") + return {"X-User-ID": uid} + +def test_coworker_full_journey(): + """ + CO-WORKER FULL JOURNEY INTEGRATION TEST: + 1. Deploy agent with High Threshold (95%) to force rework. + 2. Trigger via webhook. + 3. Monitor 'evaluation_status' for real-time indicators. + 4. Verify 'latest_quality_score' changes across attempts. + 5. Audit .cortex/history.log for: + - 'duration' fields for every step. + - 'type': 'attempt' entries for rework tracking. + - 'type': 'event' entries for Rubric/Agent execution. + 6. Ensure final result is delivered successfully. + """ + node_id = os.getenv("SYNC_TEST_NODE1", "test-node-1") + admin_id = os.getenv("SYNC_TEST_USER_ID", "9a333ccd-9c3f-432f-a030-7b1e1284a436") + instance_id = None + + # We use a prompt that is simple but specific to ensure we can verify correctness + test_prompt = "Create a file named 'secret.txt' containing the word 'PHOENIX'. Then verify it exists." + + with httpx.Client(timeout=60.0) as client: + try: + # 1. Register Node (noop if exists, but ensures it's in DB for this user) + client.post(f"{BASE_URL}/nodes/admin", params={"admin_id": admin_id}, json={ + "node_id": node_id, "is_active": True, "skill_config": {"shell": {"enabled": True}, "sync": {"enabled": True}} + }) + + # 2. Deploy Agent with High Threshold + deploy_payload = { + "name": "Full Journey Agent", + "system_prompt": "You are an infrastructure specialist.", + "max_loop_iterations": 5, + "mesh_node_id": node_id, + "provider_name": "gemini", + "model_name": "gemini-1.5-flash", + "trigger_type": "webhook", + "co_worker_quality_gate": True, + "max_rework_attempts": 2, + "rework_threshold": 98, # Extremely high to force rework + "default_prompt": test_prompt, + } + r_deploy = client.post(f"{BASE_URL}/agents/deploy", json=deploy_payload, headers=_headers()) + assert r_deploy.status_code == 200, f"Deploy failed: {r_deploy.text}" + res = r_deploy.json() + instance_id = res["instance_id"] + sync_workspace_id = res["sync_workspace_id"] + + # 3. Trigger it + r_trig = client.get(f"{BASE_URL}/agents/{instance_id}/triggers", headers=_headers()) + secret = next(t for t in r_trig.json() if t["trigger_type"] == "webhook")["webhook_secret"] + client.post(f"{BASE_URL}/agents/{instance_id}/webhook", params={"token": secret}, json={"prompt": test_prompt}) + + # 4. Monitor & Track Journey + print(f"\n[Journey] Starting tracking for Agent {instance_id}") + seen_statuses = set() + scores_log = [] + max_wait = 180 # 3 minutes total for a 2-rework journey + start_time = time.time() + + while time.time() - start_time < max_wait: + r_agents = client.get(f"{BASE_URL}/agents", headers=_headers()) + agent = next((a for a in r_agents.json() if a["id"] == instance_id), None) + if not agent: break + + status = agent.get("status") + eval_status = agent.get("evaluation_status") + score = agent.get("latest_quality_score") + + if eval_status: seen_statuses.add(eval_status) + if score and (not scores_log or scores_log[-1] != score): + scores_log.append(score) + print(f" [Metric] Quality Score updated: {score}%") + + if eval_status: + print(f" [Status] {eval_status}") + + # Terminal conditions: + if status == "idle" and eval_status in ["passed", "failed_limit"]: + print(f" [Journey] Terminal state reached: {eval_status}") + break + + if status == "error_suspended": + pytest.fail(f"Agent failed with error: {agent.get('last_error')}") + + time.sleep(3) + + # 5. Assertions on the Journey + # We expect to see various statuses throughout the loop + print(f"Seen statuses: {seen_statuses}") + # Minimum expected statuses to prove the loop happened + assert any("Analyzing" in s for s in seen_statuses), "Should have seen an Analysis phase" + + # Check if rework happened (should have if score < 98) + if any("Rework" in s for s in seen_statuses): + print(" [Verified] Rework loop was triggered.") + + # 6. Content Audit (The history.log on the node) + r_hist = client.get(f"{BASE_URL}/nodes/{node_id}/fs/cat", params={"path": ".cortex/history.log", "session_id": sync_workspace_id}, headers=_headers()) + assert r_hist.status_code == 200 + history = json.loads(r_hist.json().get("stdout", "[]")) + + assert len(history) > 0, "history.log should not be empty" + + # Verify Duration Tracking (Timespan Capture) + for entry in history: + name = entry.get("name") or entry.get("type") + duration = entry.get("duration", 0) + print(f" [Audit] Step '{name}' took {duration}s") + assert duration >= 0, f"Step {name} has invalid duration: {duration}" + + # Verify specific entries + has_rubric = any(e.get("name") == "Rubric Generation" for e in history) + has_agent_run = any(e.get("name") == "Agent execution" for e in history) + has_audit = any(e.get("name") == "Co-Worker review" for e in history) + + assert has_rubric, "Rubric generation not logged" + assert has_agent_run, "Agent execution not logged" + assert has_audit, "Audit pass not logged" + + # 7. Final Sanity Check for the generated file + r_ls = client.get(f"{BASE_URL}/nodes/{node_id}/fs/ls", params={"path": ".", "session_id": sync_workspace_id}, headers=_headers()) + files = [f["name"] for f in r_ls.json().get("files", [])] + assert "secret.txt" in files, "The agent failed to create the requested file." + + finally: + if instance_id: + client.delete(f"{BASE_URL}/agents/{instance_id}", headers=_headers()) diff --git a/browser-service/protos/browser.proto b/browser-service/protos/browser.proto index 335fc68..939d502 100644 --- a/browser-service/protos/browser.proto +++ b/browser-service/protos/browser.proto @@ -11,7 +11,7 @@ rpc Evaluate(EvalRequest) returns (BrowserResponse); rpc GetSnapshot(SnapshotRequest) returns (BrowserResponse); rpc CloseSession(CloseRequest) returns (CloseResponse); - rpc ParallelFetch(ParallelFetchRequest) returns (ParallelFetchResponse); + rpc ParallelFetch(ParallelFetchRequest) returns (stream ParallelFetchResponse); } message NavigateRequest { diff --git a/browser-service/protos/browser_pb2.py b/browser-service/protos/browser_pb2.py index 64c55f0..46631e9 100644 --- a/browser-service/protos/browser_pb2.py +++ b/browser-service/protos/browser_pb2.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! -# source: protos/browser.proto +# source: browser.proto # Protobuf Python Version: 4.25.1 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor @@ -14,39 +14,39 @@ -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x14protos/browser.proto\x12\x07\x62rowser\"K\n\x0fNavigateRequest\x12\x0b\n\x03url\x18\x01 \x01(\t\x12\x12\n\nsession_id\x18\x02 \x01(\t\x12\x17\n\x0fwait_until_idle\x18\x03 \x01(\x08\"J\n\x0c\x43lickRequest\x12\x10\n\x08selector\x18\x01 \x01(\t\x12\x12\n\nsession_id\x18\x02 \x01(\t\x12\t\n\x01x\x18\x03 \x01(\x05\x12\t\n\x01y\x18\x04 \x01(\x05\"V\n\x0bTypeRequest\x12\x10\n\x08selector\x18\x01 \x01(\t\x12\x0c\n\x04text\x18\x02 \x01(\t\x12\x12\n\nsession_id\x18\x03 \x01(\t\x12\x13\n\x0bpress_enter\x18\x04 \x01(\x08\"4\n\x0cHoverRequest\x12\x10\n\x08selector\x18\x01 \x01(\t\x12\x12\n\nsession_id\x18\x02 \x01(\t\"W\n\rScrollRequest\x12\x12\n\nsession_id\x18\x01 \x01(\t\x12\x0f\n\x07\x64\x65lta_x\x18\x02 \x01(\x05\x12\x0f\n\x07\x64\x65lta_y\x18\x03 \x01(\x05\x12\x10\n\x08selector\x18\x04 \x01(\t\"1\n\x0b\x45valRequest\x12\x0e\n\x06script\x18\x01 \x01(\t\x12\x12\n\nsession_id\x18\x02 \x01(\t\"l\n\x0fSnapshotRequest\x12\x12\n\nsession_id\x18\x01 \x01(\t\x12\x1a\n\x12include_screenshot\x18\x02 \x01(\x08\x12\x13\n\x0binclude_dom\x18\x03 \x01(\x08\x12\x14\n\x0cinclude_a11y\x18\x04 \x01(\x08\"\"\n\x0c\x43loseRequest\x12\x12\n\nsession_id\x18\x01 \x01(\t\" \n\rCloseResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\"j\n\x14ParallelFetchRequest\x12\x0c\n\x04urls\x18\x01 \x03(\t\x12\x12\n\nsession_id\x18\x02 \x01(\t\x12\x16\n\x0emax_concurrent\x18\x03 \x01(\x05\x12\x18\n\x10\x65xtract_markdown\x18\x04 \x01(\x08\"\xcd\x01\n\x15ParallelFetchResponse\x12;\n\x07results\x18\x01 \x03(\x0b\x32*.browser.ParallelFetchResponse.FetchResult\x1aw\n\x0b\x46\x65tchResult\x12\x0b\n\x03url\x18\x01 \x01(\t\x12\r\n\x05title\x18\x02 \x01(\t\x12\x18\n\x10\x63ontent_markdown\x18\x03 \x01(\t\x12\x0f\n\x07success\x18\x04 \x01(\x08\x12\r\n\x05\x65rror\x18\x05 \x01(\t\x12\x12\n\nfetch_mode\x18\x06 \x01(\t\"\xbb\x01\n\x0f\x42rowserResponse\x12\x12\n\nsession_id\x18\x01 \x01(\t\x12\x0b\n\x03url\x18\x02 \x01(\t\x12\r\n\x05title\x18\x03 \x01(\t\x12\x0e\n\x06status\x18\x04 \x01(\t\x12\x15\n\rerror_message\x18\x05 \x01(\t\x12\x10\n\x08\x64om_path\x18\x06 \x01(\t\x12\x17\n\x0fscreenshot_path\x18\x07 \x01(\t\x12\x11\n\ta11y_path\x18\x08 \x01(\t\x12\x13\n\x0b\x65val_result\x18\t \x01(\t2\xc6\x04\n\x0e\x42rowserService\x12>\n\x08Navigate\x12\x18.browser.NavigateRequest\x1a\x18.browser.BrowserResponse\x12\x38\n\x05\x43lick\x12\x15.browser.ClickRequest\x1a\x18.browser.BrowserResponse\x12\x36\n\x04Type\x12\x14.browser.TypeRequest\x1a\x18.browser.BrowserResponse\x12\x38\n\x05Hover\x12\x15.browser.HoverRequest\x1a\x18.browser.BrowserResponse\x12:\n\x06Scroll\x12\x16.browser.ScrollRequest\x1a\x18.browser.BrowserResponse\x12:\n\x08\x45valuate\x12\x14.browser.EvalRequest\x1a\x18.browser.BrowserResponse\x12\x41\n\x0bGetSnapshot\x12\x18.browser.SnapshotRequest\x1a\x18.browser.BrowserResponse\x12=\n\x0c\x43loseSession\x12\x15.browser.CloseRequest\x1a\x16.browser.CloseResponse\x12N\n\rParallelFetch\x12\x1d.browser.ParallelFetchRequest\x1a\x1e.browser.ParallelFetchResponseb\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\rbrowser.proto\x12\x07\x62rowser\"K\n\x0fNavigateRequest\x12\x0b\n\x03url\x18\x01 \x01(\t\x12\x12\n\nsession_id\x18\x02 \x01(\t\x12\x17\n\x0fwait_until_idle\x18\x03 \x01(\x08\"J\n\x0c\x43lickRequest\x12\x10\n\x08selector\x18\x01 \x01(\t\x12\x12\n\nsession_id\x18\x02 \x01(\t\x12\t\n\x01x\x18\x03 \x01(\x05\x12\t\n\x01y\x18\x04 \x01(\x05\"V\n\x0bTypeRequest\x12\x10\n\x08selector\x18\x01 \x01(\t\x12\x0c\n\x04text\x18\x02 \x01(\t\x12\x12\n\nsession_id\x18\x03 \x01(\t\x12\x13\n\x0bpress_enter\x18\x04 \x01(\x08\"4\n\x0cHoverRequest\x12\x10\n\x08selector\x18\x01 \x01(\t\x12\x12\n\nsession_id\x18\x02 \x01(\t\"W\n\rScrollRequest\x12\x12\n\nsession_id\x18\x01 \x01(\t\x12\x0f\n\x07\x64\x65lta_x\x18\x02 \x01(\x05\x12\x0f\n\x07\x64\x65lta_y\x18\x03 \x01(\x05\x12\x10\n\x08selector\x18\x04 \x01(\t\"1\n\x0b\x45valRequest\x12\x0e\n\x06script\x18\x01 \x01(\t\x12\x12\n\nsession_id\x18\x02 \x01(\t\"l\n\x0fSnapshotRequest\x12\x12\n\nsession_id\x18\x01 \x01(\t\x12\x1a\n\x12include_screenshot\x18\x02 \x01(\x08\x12\x13\n\x0binclude_dom\x18\x03 \x01(\x08\x12\x14\n\x0cinclude_a11y\x18\x04 \x01(\x08\"\"\n\x0c\x43loseRequest\x12\x12\n\nsession_id\x18\x01 \x01(\t\" \n\rCloseResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\"j\n\x14ParallelFetchRequest\x12\x0c\n\x04urls\x18\x01 \x03(\t\x12\x12\n\nsession_id\x18\x02 \x01(\t\x12\x16\n\x0emax_concurrent\x18\x03 \x01(\x05\x12\x18\n\x10\x65xtract_markdown\x18\x04 \x01(\x08\"\xcd\x01\n\x15ParallelFetchResponse\x12;\n\x07results\x18\x01 \x03(\x0b\x32*.browser.ParallelFetchResponse.FetchResult\x1aw\n\x0b\x46\x65tchResult\x12\x0b\n\x03url\x18\x01 \x01(\t\x12\r\n\x05title\x18\x02 \x01(\t\x12\x18\n\x10\x63ontent_markdown\x18\x03 \x01(\t\x12\x0f\n\x07success\x18\x04 \x01(\x08\x12\r\n\x05\x65rror\x18\x05 \x01(\t\x12\x12\n\nfetch_mode\x18\x06 \x01(\t\"\xbb\x01\n\x0f\x42rowserResponse\x12\x12\n\nsession_id\x18\x01 \x01(\t\x12\x0b\n\x03url\x18\x02 \x01(\t\x12\r\n\x05title\x18\x03 \x01(\t\x12\x0e\n\x06status\x18\x04 \x01(\t\x12\x15\n\rerror_message\x18\x05 \x01(\t\x12\x10\n\x08\x64om_path\x18\x06 \x01(\t\x12\x17\n\x0fscreenshot_path\x18\x07 \x01(\t\x12\x11\n\ta11y_path\x18\x08 \x01(\t\x12\x13\n\x0b\x65val_result\x18\t \x01(\t2\xc8\x04\n\x0e\x42rowserService\x12>\n\x08Navigate\x12\x18.browser.NavigateRequest\x1a\x18.browser.BrowserResponse\x12\x38\n\x05\x43lick\x12\x15.browser.ClickRequest\x1a\x18.browser.BrowserResponse\x12\x36\n\x04Type\x12\x14.browser.TypeRequest\x1a\x18.browser.BrowserResponse\x12\x38\n\x05Hover\x12\x15.browser.HoverRequest\x1a\x18.browser.BrowserResponse\x12:\n\x06Scroll\x12\x16.browser.ScrollRequest\x1a\x18.browser.BrowserResponse\x12:\n\x08\x45valuate\x12\x14.browser.EvalRequest\x1a\x18.browser.BrowserResponse\x12\x41\n\x0bGetSnapshot\x12\x18.browser.SnapshotRequest\x1a\x18.browser.BrowserResponse\x12=\n\x0c\x43loseSession\x12\x15.browser.CloseRequest\x1a\x16.browser.CloseResponse\x12P\n\rParallelFetch\x12\x1d.browser.ParallelFetchRequest\x1a\x1e.browser.ParallelFetchResponse0\x01\x62\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'protos.browser_pb2', _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'browser_pb2', _globals) if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None - _globals['_NAVIGATEREQUEST']._serialized_start=33 - _globals['_NAVIGATEREQUEST']._serialized_end=108 - _globals['_CLICKREQUEST']._serialized_start=110 - _globals['_CLICKREQUEST']._serialized_end=184 - _globals['_TYPEREQUEST']._serialized_start=186 - _globals['_TYPEREQUEST']._serialized_end=272 - _globals['_HOVERREQUEST']._serialized_start=274 - _globals['_HOVERREQUEST']._serialized_end=326 - _globals['_SCROLLREQUEST']._serialized_start=328 - _globals['_SCROLLREQUEST']._serialized_end=415 - _globals['_EVALREQUEST']._serialized_start=417 - _globals['_EVALREQUEST']._serialized_end=466 - _globals['_SNAPSHOTREQUEST']._serialized_start=468 - _globals['_SNAPSHOTREQUEST']._serialized_end=576 - _globals['_CLOSEREQUEST']._serialized_start=578 - _globals['_CLOSEREQUEST']._serialized_end=612 - _globals['_CLOSERESPONSE']._serialized_start=614 - _globals['_CLOSERESPONSE']._serialized_end=646 - _globals['_PARALLELFETCHREQUEST']._serialized_start=648 - _globals['_PARALLELFETCHREQUEST']._serialized_end=754 - _globals['_PARALLELFETCHRESPONSE']._serialized_start=757 - _globals['_PARALLELFETCHRESPONSE']._serialized_end=962 - _globals['_PARALLELFETCHRESPONSE_FETCHRESULT']._serialized_start=843 - _globals['_PARALLELFETCHRESPONSE_FETCHRESULT']._serialized_end=962 - _globals['_BROWSERRESPONSE']._serialized_start=965 - _globals['_BROWSERRESPONSE']._serialized_end=1152 - _globals['_BROWSERSERVICE']._serialized_start=1155 - _globals['_BROWSERSERVICE']._serialized_end=1737 + _globals['_NAVIGATEREQUEST']._serialized_start=26 + _globals['_NAVIGATEREQUEST']._serialized_end=101 + _globals['_CLICKREQUEST']._serialized_start=103 + _globals['_CLICKREQUEST']._serialized_end=177 + _globals['_TYPEREQUEST']._serialized_start=179 + _globals['_TYPEREQUEST']._serialized_end=265 + _globals['_HOVERREQUEST']._serialized_start=267 + _globals['_HOVERREQUEST']._serialized_end=319 + _globals['_SCROLLREQUEST']._serialized_start=321 + _globals['_SCROLLREQUEST']._serialized_end=408 + _globals['_EVALREQUEST']._serialized_start=410 + _globals['_EVALREQUEST']._serialized_end=459 + _globals['_SNAPSHOTREQUEST']._serialized_start=461 + _globals['_SNAPSHOTREQUEST']._serialized_end=569 + _globals['_CLOSEREQUEST']._serialized_start=571 + _globals['_CLOSEREQUEST']._serialized_end=605 + _globals['_CLOSERESPONSE']._serialized_start=607 + _globals['_CLOSERESPONSE']._serialized_end=639 + _globals['_PARALLELFETCHREQUEST']._serialized_start=641 + _globals['_PARALLELFETCHREQUEST']._serialized_end=747 + _globals['_PARALLELFETCHRESPONSE']._serialized_start=750 + _globals['_PARALLELFETCHRESPONSE']._serialized_end=955 + _globals['_PARALLELFETCHRESPONSE_FETCHRESULT']._serialized_start=836 + _globals['_PARALLELFETCHRESPONSE_FETCHRESULT']._serialized_end=955 + _globals['_BROWSERRESPONSE']._serialized_start=958 + _globals['_BROWSERRESPONSE']._serialized_end=1145 + _globals['_BROWSERSERVICE']._serialized_start=1148 + _globals['_BROWSERSERVICE']._serialized_end=1732 # @@protoc_insertion_point(module_scope) diff --git a/browser-service/protos/browser_pb2_grpc.py b/browser-service/protos/browser_pb2_grpc.py index c69dabb..0ca5947 100644 --- a/browser-service/protos/browser_pb2_grpc.py +++ b/browser-service/protos/browser_pb2_grpc.py @@ -2,7 +2,7 @@ """Client and server classes corresponding to protobuf-defined services.""" import grpc -from protos import browser_pb2 as protos_dot_browser__pb2 +import browser_pb2 as browser__pb2 class BrowserServiceStub(object): @@ -16,48 +16,48 @@ """ self.Navigate = channel.unary_unary( '/browser.BrowserService/Navigate', - request_serializer=protos_dot_browser__pb2.NavigateRequest.SerializeToString, - response_deserializer=protos_dot_browser__pb2.BrowserResponse.FromString, + request_serializer=browser__pb2.NavigateRequest.SerializeToString, + response_deserializer=browser__pb2.BrowserResponse.FromString, ) self.Click = channel.unary_unary( '/browser.BrowserService/Click', - request_serializer=protos_dot_browser__pb2.ClickRequest.SerializeToString, - response_deserializer=protos_dot_browser__pb2.BrowserResponse.FromString, + request_serializer=browser__pb2.ClickRequest.SerializeToString, + response_deserializer=browser__pb2.BrowserResponse.FromString, ) self.Type = channel.unary_unary( '/browser.BrowserService/Type', - request_serializer=protos_dot_browser__pb2.TypeRequest.SerializeToString, - response_deserializer=protos_dot_browser__pb2.BrowserResponse.FromString, + request_serializer=browser__pb2.TypeRequest.SerializeToString, + response_deserializer=browser__pb2.BrowserResponse.FromString, ) self.Hover = channel.unary_unary( '/browser.BrowserService/Hover', - request_serializer=protos_dot_browser__pb2.HoverRequest.SerializeToString, - response_deserializer=protos_dot_browser__pb2.BrowserResponse.FromString, + request_serializer=browser__pb2.HoverRequest.SerializeToString, + response_deserializer=browser__pb2.BrowserResponse.FromString, ) self.Scroll = channel.unary_unary( '/browser.BrowserService/Scroll', - request_serializer=protos_dot_browser__pb2.ScrollRequest.SerializeToString, - response_deserializer=protos_dot_browser__pb2.BrowserResponse.FromString, + request_serializer=browser__pb2.ScrollRequest.SerializeToString, + response_deserializer=browser__pb2.BrowserResponse.FromString, ) self.Evaluate = channel.unary_unary( '/browser.BrowserService/Evaluate', - request_serializer=protos_dot_browser__pb2.EvalRequest.SerializeToString, - response_deserializer=protos_dot_browser__pb2.BrowserResponse.FromString, + request_serializer=browser__pb2.EvalRequest.SerializeToString, + response_deserializer=browser__pb2.BrowserResponse.FromString, ) self.GetSnapshot = channel.unary_unary( '/browser.BrowserService/GetSnapshot', - request_serializer=protos_dot_browser__pb2.SnapshotRequest.SerializeToString, - response_deserializer=protos_dot_browser__pb2.BrowserResponse.FromString, + request_serializer=browser__pb2.SnapshotRequest.SerializeToString, + response_deserializer=browser__pb2.BrowserResponse.FromString, ) self.CloseSession = channel.unary_unary( '/browser.BrowserService/CloseSession', - request_serializer=protos_dot_browser__pb2.CloseRequest.SerializeToString, - response_deserializer=protos_dot_browser__pb2.CloseResponse.FromString, + request_serializer=browser__pb2.CloseRequest.SerializeToString, + response_deserializer=browser__pb2.CloseResponse.FromString, ) - self.ParallelFetch = channel.unary_unary( + self.ParallelFetch = channel.unary_stream( '/browser.BrowserService/ParallelFetch', - request_serializer=protos_dot_browser__pb2.ParallelFetchRequest.SerializeToString, - response_deserializer=protos_dot_browser__pb2.ParallelFetchResponse.FromString, + request_serializer=browser__pb2.ParallelFetchRequest.SerializeToString, + response_deserializer=browser__pb2.ParallelFetchResponse.FromString, ) @@ -123,48 +123,48 @@ rpc_method_handlers = { 'Navigate': grpc.unary_unary_rpc_method_handler( servicer.Navigate, - request_deserializer=protos_dot_browser__pb2.NavigateRequest.FromString, - response_serializer=protos_dot_browser__pb2.BrowserResponse.SerializeToString, + request_deserializer=browser__pb2.NavigateRequest.FromString, + response_serializer=browser__pb2.BrowserResponse.SerializeToString, ), 'Click': grpc.unary_unary_rpc_method_handler( servicer.Click, - request_deserializer=protos_dot_browser__pb2.ClickRequest.FromString, - response_serializer=protos_dot_browser__pb2.BrowserResponse.SerializeToString, + request_deserializer=browser__pb2.ClickRequest.FromString, + response_serializer=browser__pb2.BrowserResponse.SerializeToString, ), 'Type': grpc.unary_unary_rpc_method_handler( servicer.Type, - request_deserializer=protos_dot_browser__pb2.TypeRequest.FromString, - response_serializer=protos_dot_browser__pb2.BrowserResponse.SerializeToString, + request_deserializer=browser__pb2.TypeRequest.FromString, + response_serializer=browser__pb2.BrowserResponse.SerializeToString, ), 'Hover': grpc.unary_unary_rpc_method_handler( servicer.Hover, - request_deserializer=protos_dot_browser__pb2.HoverRequest.FromString, - response_serializer=protos_dot_browser__pb2.BrowserResponse.SerializeToString, + request_deserializer=browser__pb2.HoverRequest.FromString, + response_serializer=browser__pb2.BrowserResponse.SerializeToString, ), 'Scroll': grpc.unary_unary_rpc_method_handler( servicer.Scroll, - request_deserializer=protos_dot_browser__pb2.ScrollRequest.FromString, - response_serializer=protos_dot_browser__pb2.BrowserResponse.SerializeToString, + request_deserializer=browser__pb2.ScrollRequest.FromString, + response_serializer=browser__pb2.BrowserResponse.SerializeToString, ), 'Evaluate': grpc.unary_unary_rpc_method_handler( servicer.Evaluate, - request_deserializer=protos_dot_browser__pb2.EvalRequest.FromString, - response_serializer=protos_dot_browser__pb2.BrowserResponse.SerializeToString, + request_deserializer=browser__pb2.EvalRequest.FromString, + response_serializer=browser__pb2.BrowserResponse.SerializeToString, ), 'GetSnapshot': grpc.unary_unary_rpc_method_handler( servicer.GetSnapshot, - request_deserializer=protos_dot_browser__pb2.SnapshotRequest.FromString, - response_serializer=protos_dot_browser__pb2.BrowserResponse.SerializeToString, + request_deserializer=browser__pb2.SnapshotRequest.FromString, + response_serializer=browser__pb2.BrowserResponse.SerializeToString, ), 'CloseSession': grpc.unary_unary_rpc_method_handler( servicer.CloseSession, - request_deserializer=protos_dot_browser__pb2.CloseRequest.FromString, - response_serializer=protos_dot_browser__pb2.CloseResponse.SerializeToString, + request_deserializer=browser__pb2.CloseRequest.FromString, + response_serializer=browser__pb2.CloseResponse.SerializeToString, ), - 'ParallelFetch': grpc.unary_unary_rpc_method_handler( + 'ParallelFetch': grpc.unary_stream_rpc_method_handler( servicer.ParallelFetch, - request_deserializer=protos_dot_browser__pb2.ParallelFetchRequest.FromString, - response_serializer=protos_dot_browser__pb2.ParallelFetchResponse.SerializeToString, + request_deserializer=browser__pb2.ParallelFetchRequest.FromString, + response_serializer=browser__pb2.ParallelFetchResponse.SerializeToString, ), } generic_handler = grpc.method_handlers_generic_handler( @@ -188,8 +188,8 @@ timeout=None, metadata=None): return grpc.experimental.unary_unary(request, target, '/browser.BrowserService/Navigate', - protos_dot_browser__pb2.NavigateRequest.SerializeToString, - protos_dot_browser__pb2.BrowserResponse.FromString, + browser__pb2.NavigateRequest.SerializeToString, + browser__pb2.BrowserResponse.FromString, options, channel_credentials, insecure, call_credentials, compression, wait_for_ready, timeout, metadata) @@ -205,8 +205,8 @@ timeout=None, metadata=None): return grpc.experimental.unary_unary(request, target, '/browser.BrowserService/Click', - protos_dot_browser__pb2.ClickRequest.SerializeToString, - protos_dot_browser__pb2.BrowserResponse.FromString, + browser__pb2.ClickRequest.SerializeToString, + browser__pb2.BrowserResponse.FromString, options, channel_credentials, insecure, call_credentials, compression, wait_for_ready, timeout, metadata) @@ -222,8 +222,8 @@ timeout=None, metadata=None): return grpc.experimental.unary_unary(request, target, '/browser.BrowserService/Type', - protos_dot_browser__pb2.TypeRequest.SerializeToString, - protos_dot_browser__pb2.BrowserResponse.FromString, + browser__pb2.TypeRequest.SerializeToString, + browser__pb2.BrowserResponse.FromString, options, channel_credentials, insecure, call_credentials, compression, wait_for_ready, timeout, metadata) @@ -239,8 +239,8 @@ timeout=None, metadata=None): return grpc.experimental.unary_unary(request, target, '/browser.BrowserService/Hover', - protos_dot_browser__pb2.HoverRequest.SerializeToString, - protos_dot_browser__pb2.BrowserResponse.FromString, + browser__pb2.HoverRequest.SerializeToString, + browser__pb2.BrowserResponse.FromString, options, channel_credentials, insecure, call_credentials, compression, wait_for_ready, timeout, metadata) @@ -256,8 +256,8 @@ timeout=None, metadata=None): return grpc.experimental.unary_unary(request, target, '/browser.BrowserService/Scroll', - protos_dot_browser__pb2.ScrollRequest.SerializeToString, - protos_dot_browser__pb2.BrowserResponse.FromString, + browser__pb2.ScrollRequest.SerializeToString, + browser__pb2.BrowserResponse.FromString, options, channel_credentials, insecure, call_credentials, compression, wait_for_ready, timeout, metadata) @@ -273,8 +273,8 @@ timeout=None, metadata=None): return grpc.experimental.unary_unary(request, target, '/browser.BrowserService/Evaluate', - protos_dot_browser__pb2.EvalRequest.SerializeToString, - protos_dot_browser__pb2.BrowserResponse.FromString, + browser__pb2.EvalRequest.SerializeToString, + browser__pb2.BrowserResponse.FromString, options, channel_credentials, insecure, call_credentials, compression, wait_for_ready, timeout, metadata) @@ -290,8 +290,8 @@ timeout=None, metadata=None): return grpc.experimental.unary_unary(request, target, '/browser.BrowserService/GetSnapshot', - protos_dot_browser__pb2.SnapshotRequest.SerializeToString, - protos_dot_browser__pb2.BrowserResponse.FromString, + browser__pb2.SnapshotRequest.SerializeToString, + browser__pb2.BrowserResponse.FromString, options, channel_credentials, insecure, call_credentials, compression, wait_for_ready, timeout, metadata) @@ -307,8 +307,8 @@ timeout=None, metadata=None): return grpc.experimental.unary_unary(request, target, '/browser.BrowserService/CloseSession', - protos_dot_browser__pb2.CloseRequest.SerializeToString, - protos_dot_browser__pb2.CloseResponse.FromString, + browser__pb2.CloseRequest.SerializeToString, + browser__pb2.CloseResponse.FromString, options, channel_credentials, insecure, call_credentials, compression, wait_for_ready, timeout, metadata) @@ -323,8 +323,8 @@ wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary(request, target, '/browser.BrowserService/ParallelFetch', - protos_dot_browser__pb2.ParallelFetchRequest.SerializeToString, - protos_dot_browser__pb2.ParallelFetchResponse.FromString, + return grpc.experimental.unary_stream(request, target, '/browser.BrowserService/ParallelFetch', + browser__pb2.ParallelFetchRequest.SerializeToString, + browser__pb2.ParallelFetchResponse.FromString, options, channel_credentials, insecure, call_credentials, compression, wait_for_ready, timeout, metadata) diff --git a/browser-service/src/api/servicer.py b/browser-service/src/api/servicer.py index 96ce5bd..758b8db 100644 --- a/browser-service/src/api/servicer.py +++ b/browser-service/src/api/servicer.py @@ -250,23 +250,31 @@ max_concurrent = request.max_concurrent or 5 extract_markdown = request.extract_markdown - logger.info(f"Parallel fetching {len(urls)} URLs (max_concurrent={max_concurrent})") + logger.info(f"Stream-fetching {len(urls)} URLs (max_concurrent={max_concurrent})") - results = await self.browser.parallel_fetch( - urls, - max_concurrent=max_concurrent, - extract_markdown=extract_markdown - ) + # We wrap the underlying parallel_fetch to yield items as they complete + # instead of gathering them all at once. + tasks = [self.browser._fetch_one_internal(url, extract_markdown) for url in urls] - proto_results = [] - for r in results: - proto_results.append(browser_pb2.ParallelFetchResponse.FetchResult( - url=r["url"], - title=r.get("title", ""), - content_markdown=r.get("content_markdown", ""), - success=r["success"], - error=r.get("error", ""), - fetch_mode=r.get("fetch_mode", "") - )) - - return browser_pb2.ParallelFetchResponse(results=proto_results) + # Use a semaphore to respect max_concurrent + semaphore = asyncio.Semaphore(max_concurrent) + + async def wrapped_fetch(task): + async with semaphore: + return await task + + # Create the wrapped tasks + wrapped_tasks = [wrapped_fetch(t) for t in tasks] + + for future in asyncio.as_completed(wrapped_tasks): + r = await future + yield browser_pb2.ParallelFetchResponse(results=[ + browser_pb2.ParallelFetchResponse.FetchResult( + url=r["url"], + title=r.get("title", ""), + content_markdown=r.get("content_markdown", ""), + success=r["success"], + error=r.get("error", ""), + fetch_mode=r.get("fetch_mode", "") + ) + ]) diff --git a/browser-service/src/core/browser.py b/browser-service/src/core/browser.py index 59e978d..12817cc 100644 --- a/browser-service/src/core/browser.py +++ b/browser-service/src/core/browser.py @@ -168,6 +168,76 @@ return False + async def _fetch_one_internal(self, url, extract_markdown=True): + """Internal worker logic for a single URL fetch.""" + logger.info(f"Worker fetching: {url}") + + # Fast static fetch path (no browser start) + static_result = await self._static_fetch(url, extract_markdown=extract_markdown) + if static_result.get("success"): + if not self._needs_js_render( + static_result.get("html", ""), + static_result.get("content_markdown", ""), + static_result.get("title", ""), + ): + logger.info(f"Static fetch sufficient for: {url} (fetch_mode=static)") + return { + "url": url, + "title": static_result.get("title", ""), + "content_markdown": static_result.get("content_markdown", ""), + "success": True, + "fetch_mode": "static", + } + else: + logger.info(f"Static fetch looked like JS shell, falling back to browser for: {url} (fetch_mode=js)") + else: + logger.info(f"Static fetch failed for {url}: {static_result.get('error')}. Falling back to browser.") + + # Fall back to Playwright rendering + # Separate context for each fetch for isolation + try: + context = await asyncio.wait_for( + self._browser.new_context( + viewport={'width': 1280, 'height': 800}, + user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36' + ), timeout=20.0 + ) + page = await asyncio.wait_for(context.new_page(), timeout=20.0) + except Exception as ce: + logger.warning(f"Browser stall during parallel fetch context: {ce}. Falling back to error state.") + return {"url": url, "success": False, "error": f"Failed to initialize Chromium context locally: {ce}", "fetch_mode": "js"} + try: + await page.goto(url, wait_until="domcontentloaded", timeout=20000) + await asyncio.sleep(1) # Wait for JS dynamic content + title = await page.title() + + content = "" + if extract_markdown: + html = await page.content() + # Keep existing extractor behavior + from src.extraction.markdown import MarkdownExtractor + extractor = MarkdownExtractor() + content = extractor.extract(html) + + return { + "url": url, + "title": title, + "content_markdown": content, + "success": True, + "fetch_mode": "js" + } + except Exception as e: + logger.warning(f"Failed to fetch {url}: {e} (fetch_mode=js)") + return { + "url": url, + "success": False, + "error": str(e), + "fetch_mode": "js" + } + finally: + if 'context' in locals(): + await context.close() + async def parallel_fetch(self, urls, max_concurrent=5, extract_markdown=True): """Fetches multiple URLs in parallel using a pool of pages.""" import asyncio @@ -177,73 +247,7 @@ async def fetch_one(url): async with semaphore: - logger.info(f"Worker fetching: {url}") - - # Fast static fetch path (no browser start) - static_result = await self._static_fetch(url, extract_markdown=extract_markdown) - if static_result.get("success"): - if not self._needs_js_render( - static_result.get("html", ""), - static_result.get("content_markdown", ""), - static_result.get("title", ""), - ): - logger.info(f"Static fetch sufficient for: {url} (fetch_mode=static)") - return { - "url": url, - "title": static_result.get("title", ""), - "content_markdown": static_result.get("content_markdown", ""), - "success": True, - "fetch_mode": "static", - } - else: - logger.info(f"Static fetch looked like JS shell, falling back to browser for: {url} (fetch_mode=js)") - else: - logger.info(f"Static fetch failed for {url}: {static_result.get('error')}. Falling back to browser.") - - # Fall back to Playwright rendering - # Separate context for each fetch for isolation - try: - context = await asyncio.wait_for( - self._browser.new_context( - viewport={'width': 1280, 'height': 800}, - user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36' - ), timeout=20.0 - ) - page = await asyncio.wait_for(context.new_page(), timeout=20.0) - except Exception as ce: - logger.warning(f"Browser stall during parallel fetch context: {ce}. Falling back to error state.") - return {"url": url, "success": False, "error": f"Failed to initialize Chromium context locally: {ce}", "fetch_mode": "js"} - try: - await page.goto(url, wait_until="domcontentloaded", timeout=20000) - await asyncio.sleep(1) # Wait for JS dynamic content - title = await page.title() - - content = "" - if extract_markdown: - html = await page.content() - # Keep existing extractor behavior - from src.extraction.markdown import MarkdownExtractor - extractor = MarkdownExtractor() - content = extractor.extract(html) - - return { - "url": url, - "title": title, - "content_markdown": content, - "success": True, - "fetch_mode": "js" - } - except Exception as e: - logger.warning(f"Failed to fetch {url}: {e} (fetch_mode=js)") - return { - "url": url, - "success": False, - "error": str(e), - "fetch_mode": "js" - } - finally: - if 'context' in locals(): - await context.close() + return await self._fetch_one_internal(url, extract_markdown) tasks = [fetch_one(url) for url in urls] return await asyncio.gather(*tasks) diff --git a/frontend/src/features/agents/components/AgentDrillDown.js b/frontend/src/features/agents/components/AgentDrillDown.js index 82910fe..e1fedbb 100644 --- a/frontend/src/features/agents/components/AgentDrillDown.js +++ b/frontend/src/features/agents/components/AgentDrillDown.js @@ -39,6 +39,11 @@ const [historyLog, setHistoryLog] = useState([]); const [savingGroundTruth, setSavingGroundTruth] = useState(false); + // Monitoring & Timer States + const [runningSeconds, setRunningSeconds] = useState(0); + const [lastTotalConsumption, setLastTotalConsumption] = useState(null); + const [previousStatus, setPreviousStatus] = useState('idle'); + // Helper: Convert cron expression to human-readable text const describeCron = (expr) => { if (!expr) return ''; @@ -206,6 +211,25 @@ return () => clearInterval(interval); }, [agentId]); + useEffect(() => { + let interval = null; + if (agent?.status === 'active') { + // Reset timer if we just transitioned to active + if (previousStatus !== 'active') { + setRunningSeconds(0); + setLastTotalConsumption(null); + } + interval = setInterval(() => { + setRunningSeconds(s => s + 1); + }, 1000); + } else if (previousStatus === 'active' && agent?.status !== 'active') { + // Captured finished state + setLastTotalConsumption(runningSeconds); + } + setPreviousStatus(agent?.status || 'idle'); + return () => clearInterval(interval); + }, [agent?.status, runningSeconds, previousStatus]); + const [skipEval, setSkipEval] = useState(false); const handleInjectOverride = async (e) => { @@ -521,17 +545,48 @@ {/* Agent Status Indicator Label (M4 Observability) */} - {agent?.status === 'active' && ( -
-
-
-
+ {(agent?.status === 'active' || lastTotalConsumption !== null) && ( +
+
+
+
+ {agent?.status === 'active' &&
} +
+
+ + {agent?.status === 'active' ? "Internal Trace:" : "Last Action:"} + + + {agent?.status === 'active' + ? (agent.evaluation_status + ? (agent.evaluation_status.toLowerCase().includes('audit') || agent.evaluation_status.toLowerCase().includes('worker') + ? `🛡️ Co-Worker Audit: ${agent.evaluation_status}` + : `🤖 Main Agent: ${agent.evaluation_status}`) + : '🤖 Main Agent: Orchestrating task payload...') + : '🏁 Task execution cycle completed successfully.'} + +
- - Activity: - {(agent.evaluation_status && agent.evaluation_status.length > 10) ? agent.evaluation_status : 'Executing task instructions...'} - - +
+ {agent?.status === 'active' ? ( +
+
+ {runningSeconds}s +
+ ) : ( +
+ + {lastTotalConsumption}s tot. +
+ )} +
)} diff --git a/frontend/src/features/agents/components/AgentHarnessPage.js b/frontend/src/features/agents/components/AgentHarnessPage.js index ffb1c0e..7f3271e 100644 --- a/frontend/src/features/agents/components/AgentHarnessPage.js +++ b/frontend/src/features/agents/components/AgentHarnessPage.js @@ -436,6 +436,11 @@ const [triggers, setTriggers] = useState([]); const [copiedId, setCopiedId] = useState(null); + // Live Trace States + const [runningSeconds, setRunningSeconds] = useState(0); + const [lastTotalConsumption, setLastTotalConsumption] = useState(null); + const [previousStatus, setPreviousStatus] = useState(agent.status || 'idle'); + useEffect(() => { let isMounted = true; const fetchTriggers = async () => { @@ -448,6 +453,23 @@ return () => { isMounted = false; }; }, [agent.id]); + useEffect(() => { + let interval = null; + if (agent.status === 'active') { + if (previousStatus !== 'active') { + setRunningSeconds(0); + setLastTotalConsumption(null); + } + interval = setInterval(() => { + setRunningSeconds(s => s + 1); + }, 1000); + } else if (previousStatus === 'active' && agent.status !== 'active') { + setLastTotalConsumption(runningSeconds); + } + setPreviousStatus(agent.status || 'idle'); + return () => clearInterval(interval); + }, [agent.status, runningSeconds, previousStatus]); + const handleAction = async (status) => { try { await updateAgentStatus(agent.id, status); @@ -541,12 +563,22 @@ {agent.status}
{agent.evaluation_status && ( -
- {agent.evaluation_status === 'reworking' ? `Reworking (${agent.current_rework_attempt}/${agent.template?.max_rework_attempts || 3})` : agent.evaluation_status} +
+ {(agent.status === 'active' || lastTotalConsumption) && ( +
+ + {agent.status === 'active' ? `${runningSeconds}s` : `${lastTotalConsumption}s`} +
+ )} +
+ {agent.evaluation_status.includes('reworking') ? `Reworking (${agent.current_rework_attempt}/${agent.template?.max_rework_attempts || 3})` : agent.evaluation_status} +
)}
diff --git a/frontend/src/features/chat/components/ChatWindow.js b/frontend/src/features/chat/components/ChatWindow.js index 7e13e0a..2d59cd9 100644 --- a/frontend/src/features/chat/components/ChatWindow.js +++ b/frontend/src/features/chat/components/ChatWindow.js @@ -148,19 +148,30 @@ {!message.isUser && evaluationMetadata && (evaluationMetadata.rubric || evaluationMetadata.feedback || (evaluationMetadata.history && evaluationMetadata.history.length > 0)) && (
- - - 📜 View Quality Audit & Evaluation + +
+ + 📜 View Quality Audit & Evaluation +
+ e.stopPropagation()} + > + OPEN EVALUATION HUB ↗ +
-
+
{/* Rubric snippet */} {evaluationMetadata.rubric && ( -
-
- 📜 Quality Rubric +
+
+
📝 Quality Rubric
-
+
{evaluationMetadata.rubric}
@@ -168,11 +179,11 @@ {/* Feedback snippet */} {evaluationMetadata.feedback && ( -
-
-
Co-Worker Feedback +
+
+
🤖 Co-Worker Feedback
-
+
{evaluationMetadata.feedback}
@@ -181,21 +192,21 @@ {/* History Timeline snippet */} {evaluationMetadata.history && evaluationMetadata.history.length > 0 && ( -
-
- 🕒 Rework History Timeline +
+
+
🕒 Rework History Timeline
-
+
{evaluationMetadata.history.map((h, i) => ( -
-
- +
+
+ {h.type === 'attempt' ? `Attempt ${h.round}` : h.name} - {h.score !== undefined && Score: {h.score}%} + {h.score !== undefined && Score: {h.score}%} - {h.reason || h.message || h.details} + {h.reason || h.message || h.details}
- + {h.timestamp ? new Date(h.timestamp * (h.timestamp < 2000000000 ? 1000 : 1)).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : 'Recent'}
diff --git a/frontend_ui_audit.md b/frontend_ui_audit.md new file mode 100644 index 0000000..e9c90a9 --- /dev/null +++ b/frontend_ui_audit.md @@ -0,0 +1,37 @@ +# Frontend UI Audit Report + +This audit was conducted to identify non-functional UI elements, placeholder features, and backend-supported features that are currently missing from the user interface. + +## 1. Purposeless Placeholder Elements +These elements are visible in the UI but are explicitly disabled or have no functional logic connected to them. + +| Feature | Location | Status | Rationale | +| :--- | :--- | :--- | :--- | +| **History** | Navbar | `disabled: true` | Strategic placeholder for future session/activity history. | +| **Favorites** | Navbar | `disabled: true` | Strategic placeholder for future bookmarking of agents or conversations. | + +## 2. Unexposed Backend Features (API Gaps) +These features exist in the backend services (`services/api/`) but are not currently implemented in any UI page or component. + +| Service Function | API Endpoint | Description | +| :--- | :--- | :--- | +| `uploadDocument` | `POST /rag/documents` | The backend supports document ingestion for RAG, but there is no "Knowledge Base" or "Upload" UI in the dashboard. | +| `adminDownloadNodeBundle` | `POST /nodes/setup-bundle` | Orchestrates a zip download for node setup; not directly used in any navigation menu. | +| `adminProvisionNode` | `POST /nodes/provision` | Automated provisioning logic that is currently handled via the manual bash "One-Liner" instead of a UI wizard. | + +## 3. Fully Functional High-Value Features +The following major modules were audited and confirmed as **fully integrated** with the backend: + +* **Agent Control Hub**: Full CRUD, deployment, and lifecycle management (Pause/Resume/Delete) are functional. +* **Agent Metrics**: Real-time aggregation of token usage, execution time, and tool call success/failure rates is operational. +* **Swarm Control**: Node attachment, workspace synchronization (Master-Worker), and collaborative task execution are fully functional. +* **Evaluation Hub**: The "Coworker Audit" system (Quality Gates) is active, including ground-truth alignment and feedback loops. +* **Mesh Management**: Live WebSocket monitoring of mesh nodes and one-liner deployment generation are functional. +* **Voice Interface**: Real-time STT/TTS streaming with multi-provider support (Gemini, OpenAI, ElevenLabs, etc.) is functional. + +## 4. Minor UI Implementation Notes +* **Metrics Cards**: The cards for "AI Run Time" and "Tokens" in the Agent Drilldown have interactive "flip" animations that reveal historical averages. While functional, they represent a high density of information that may be overlooked by users. +* **Skill Management**: The library is functional, but lacks a "Template Gallery" (all skills are currently custom or system-defined). + +> [!NOTE] +> The frontend is exceptionally well-connected to the current API surface. The primary "Ghost" features are documented above as strategic placeholders for the next phase of development. diff --git a/run_integration_tests.sh b/run_integration_tests.sh index 4a5d572..9bf7bb6 100755 --- a/run_integration_tests.sh +++ b/run_integration_tests.sh @@ -23,9 +23,15 @@ # Parse flags NO_REBUILD=false -if [[ "$*" == *"--no-rebuild"* ]]; then - NO_REBUILD=true -fi +NATIVE_MODE=false +for arg in "$@"; do + if [[ "$arg" == "--no-rebuild" ]]; then + NO_REBUILD=true + fi + if [[ "$arg" == "--native" ]]; then + NATIVE_MODE=true + fi +done # Check if docker daemon is reachable (i.e., not inside DevContainer without DIND) or if specifically skipped DOCKER_AVAILABLE=false @@ -103,7 +109,7 @@ echo "==========================================" echo " EXECUTING E2E INTEGRATION SUITE " echo "==========================================" -if [ "$NATIVE" = 1 ]; then +if [ "$NATIVE_MODE" = true ] || [ "$NATIVE" = 1 ]; then export SYNC_TEST_BASE_URL="http://127.0.0.1:8000/api/v1" export TEST_HUB_URL="http://127.0.0.1:8000" export TEST_GRPC_ENDPOINT="127.0.0.1:50051" @@ -112,7 +118,7 @@ TEST_TARGETS=() for arg in "$@"; do - if [[ "$arg" != "--no-rebuild" ]]; then + if [[ "$arg" != "--no-rebuild" ]] && [[ "$arg" != "--native" ]]; then TEST_TARGETS+=("$arg") fi done