From a258651c683bdb5a4054f88a4704cd942a28604e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Louis-F=C3=A9lix=20Nothias?= Date: Mon, 29 Jun 2026 14:48:10 +0200 Subject: [PATCH 1/3] fix(core): bound MCP discovery retry on total wait time, not attempt count --- sources/core/factory.py | 38 +++++++++++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/sources/core/factory.py b/sources/core/factory.py index 30bf868..0b84f5f 100644 --- a/sources/core/factory.py +++ b/sources/core/factory.py @@ -47,22 +47,46 @@ async def load_tools_code(self) -> tuple[str, str]: """ tools_code = "" existing_tool_prompt = "" + import asyncio as _asyncio + import time as _time tool_manager = ToolManager(self.config) - try: - tool_setup = False - while tool_setup == False: + # MCP servers can take minutes to come up (Docker image builds, cold + # starts, a server brought up by hand). Bound on total wait, not on a + # fixed attempt count. Exponential backoff capped at 30 s per wait. + max_wait_seconds = 300.0 + backoff = 1.0 + deadline = _time.monotonic() + max_wait_seconds + attempt = 0 + mcps = [] + while True: + attempt += 1 + try: mcps = await tool_manager.discover_mcp_servers() tool_setup = await tool_manager.verify_tools() - except Exception as e: - self.logger.error(f"load_tools_code: Failed to discover MCP servers: {str(e)}") - raise RuntimeError(f"Failed to discover MCP servers: {str(e)}") from e + if tool_setup and mcps: + break + reason = "MCP servers not ready yet" + except Exception as e: + reason = str(e) + if _time.monotonic() >= deadline: + self.logger.error( + f"load_tools_code: MCP discovery failed after {max_wait_seconds:.0f}s " + f"({attempt} attempts): {reason}" + ) + raise RuntimeError( + f"MCP servers did not become ready within {max_wait_seconds:.0f}s: {reason}" + ) + self.logger.warning( + f"MCP discovery attempt {attempt} failed ({reason}). Retrying in {backoff:.1f}s…" + ) + await _asyncio.sleep(backoff) + backoff = min(backoff * 2, 30.0) if not mcps: raise ValueError( "\n" + "=" * 80 + "\n🚨 FATAL ERROR: No MCP Servers Found! 🚨" "\n" + "-" * 80 + "\nPlease ensure at least one MCP instance is running on Toolomics." - "\nRetrying until MCPs detected.... use CTRL+C to stop." "\n" + "=" * 80 + "\n" ) for mcp in mcps: From f3f1a35cf74acec666af2fe8228175d19a70776e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Louis-F=C3=A9lix=20Nothias?= Date: Mon, 29 Jun 2026 14:48:10 +0200 Subject: [PATCH 2/3] fix(core): keep reviewed evolution, orchestrator, planner and workflow guards --- sources/core/evolution_engine.py | 6 +++++- sources/core/orchestrator.py | 11 +++++++++-- sources/core/planner.py | 8 ++++++++ sources/core/workflow_factory.py | 16 +++++++++++++++- sources/transparency/astra_exporter.py | 3 +++ 5 files changed, 40 insertions(+), 4 deletions(-) diff --git a/sources/core/evolution_engine.py b/sources/core/evolution_engine.py index 02801e2..004a1a2 100644 --- a/sources/core/evolution_engine.py +++ b/sources/core/evolution_engine.py @@ -360,8 +360,12 @@ async def start_workflow_evolution( wf = None max_iteration = self.config.max_learning_evolve_iterations if enable_evolution else 1 - # Reset archive at session start + # Reset archive and variation history at session start so each task + # starts with a clean slate (prevents boldness contamination across rows). self.selection._archive = [] + self.variation.score_history.clear() + self.variation.textual_gradient_history.clear() + self.variation.agent_count_history.clear() parents, _ = self.select_parent_workflow( goal, template_uuid=template_uuid diff --git a/sources/core/orchestrator.py b/sources/core/orchestrator.py index c78738e..e89cfe1 100644 --- a/sources/core/orchestrator.py +++ b/sources/core/orchestrator.py @@ -89,8 +89,15 @@ def progress_handler(line: str) -> None: result.stdout or result.stderr or "No output from workflow execution." ) else: - print_err(f"Workflow execution failed: {result.stderr}") - raise Exception(f"Workflow execution failed: {result.stderr}") + stderr = result.stderr or "" + if "TimeoutExpired" in stderr or "timed out" in stderr.lower(): + error_type = "TIMEOUT" + elif "SyntaxError" in stderr: + error_type = "SYNTAX_ERROR" + else: + error_type = "RUNTIME_ERROR" + print_err(f"[{error_type}] Workflow execution failed: {stderr}") + raise Exception(f"[{error_type}] Workflow execution failed: {stderr}") def perspicacite_grounding_task(self, task: str) -> str: """Query Perspicacite-AI for a literature-grounded approach to a task. diff --git a/sources/core/planner.py b/sources/core/planner.py index 214c7f2..f2cc24a 100644 --- a/sources/core/planner.py +++ b/sources/core/planner.py @@ -656,6 +656,14 @@ def _can_execute_step(self, step: PlanStep) -> tuple[bool, list[str]]: dep_task = next((task for task in self.task_history if task.name == dep_name), None) if dep_task is None or dep_task.status != TaskStatus.COMPLETED: missing_deps.append(dep_name) + continue + expected_outputs = getattr(dep_task, "expected_outputs", None) + if expected_outputs: + outputs_ok, missing_outputs = self._verify_expected_outputs(expected_outputs) + if not outputs_ok: + missing_deps.append( + f"{dep_name}[missing_outputs:{','.join(missing_outputs)}]" + ) return len(missing_deps) == 0, missing_deps def request_user_exit(self, msg: str) -> None: diff --git a/sources/core/workflow_factory.py b/sources/core/workflow_factory.py index aab03c3..5426edf 100644 --- a/sources/core/workflow_factory.py +++ b/sources/core/workflow_factory.py @@ -265,6 +265,12 @@ def validate_workflow_structure(self, workflow_genotype_code: str) -> None: nodes = set(re.findall(patterns["nodes"], workflow_genotype_code)) if not nodes: raise ValueError("No workflow nodes found") + import keyword as _keyword + invalid_ids = {n for n in nodes if _keyword.iskeyword(n) or not n.isidentifier()} + if invalid_ids: + raise ValueError( + f"Workflow node names are Python keywords or invalid identifiers: {sorted(invalid_ids)}" + ) self.logger.debug(f"Workflow nodes discovered: {', '.join(sorted(nodes))}") # Validate START edge target exists @@ -389,8 +395,11 @@ def assemble_workflow( if WORKFLOW_PATH: print("workflow run: saving workflow state JSON at :", WORKFLOW_PATH) try: - with open(os.path.join(WORKFLOW_PATH, "state_result.json"), "w") as f: + _state_path = os.path.join(WORKFLOW_PATH, "state_result.json") + _tmp_path = _state_path + ".tmp" + with open(_tmp_path, "w") as f: json.dump(result_state, f, indent=2) + os.replace(_tmp_path, _state_path) except Exception as e: raise(Exception(f"Could not save workflow data:" + str(e))) """ @@ -487,6 +496,11 @@ async def craft_workflow( smolagent_system_prompt ) + try: + compile(complete_code, "", "exec") + except SyntaxError as e: + raise ValueError(f"UUID:{uuid_str}|Assembled workflow has invalid syntax: {e}") from e + self.logger.info("Workflow generation completed") self.logger.debug(f"Workflow path: {workflow_path}") diff --git a/sources/transparency/astra_exporter.py b/sources/transparency/astra_exporter.py index 851b0d5..93885a2 100644 --- a/sources/transparency/astra_exporter.py +++ b/sources/transparency/astra_exporter.py @@ -78,6 +78,9 @@ def export(self, best_uuid: str, goal: str) -> Path | None: return None raw_steps = load_trace(memory_path) + if not raw_steps: + print_warn(f"Empty trace for {best_uuid}; ASTRA export skipped.") + return None compact = compact_trace(raw_steps) print_info( f"Trace compacted: {len(raw_steps)} raw → {len(compact)} candidate steps." From fc4fd0878c0ccb5420ccc216c7c717f115d0de42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Louis-F=C3=A9lix=20Nothias?= Date: Mon, 29 Jun 2026 14:48:10 +0200 Subject: [PATCH 3/3] fix(llm): omit temperature for all Anthropic models instead of version-gating --- sources/core/llm_provider.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sources/core/llm_provider.py b/sources/core/llm_provider.py index 1fd9576..eb96eea 100644 --- a/sources/core/llm_provider.py +++ b/sources/core/llm_provider.py @@ -445,11 +445,14 @@ def __call__(self, prompt: str, timeout: int = 180, use_cache: bool = True) -> s completion_params = { "model": f"{self.config.provider}/{self.config.model}", "messages": self._apply_cache_control(message), - "temperature": effective_temperature, "timeout": timeout, "max_tokens": self.config.max_tokens, "drop_params": True, } + # Anthropic models reject (Opus 4.x) or ignore an explicit + # temperature; omit it for all of them rather than version-gate. + if not self._is_claude_model(): + completion_params["temperature"] = effective_temperature completion_params["api_key"] = self.config.key # Add reasoning effort if supported (not for Claude models) if self._supports_reasoning_tokens() and not self._is_claude_model():