Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion sources/core/evolution_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
38 changes: 31 additions & 7 deletions sources/core/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
5 changes: 4 additions & 1 deletion sources/core/llm_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down
11 changes: 9 additions & 2 deletions sources/core/orchestrator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
8 changes: 8 additions & 0 deletions sources/core/planner.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
16 changes: 15 additions & 1 deletion sources/core/workflow_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)))
"""
Expand Down Expand Up @@ -487,6 +496,11 @@ async def craft_workflow(
smolagent_system_prompt
)

try:
compile(complete_code, "<assembled_workflow>", "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}")
Expand Down
3 changes: 3 additions & 0 deletions sources/transparency/astra_exporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."
Expand Down