#4893 Folder/File-Based Skill, Drag-n-Drop AI Agent Workflow Definition

#4893 Folder/File-Based Skill, Drag-n-Drop AI Agent Workflow Definition

  1. Application goal: Development of a reusable, easy to define and update skill library in folder and text file format (including .md, .json and .py). Easy selection of skills to assign to AI agents. Easy selection of AI agents to apply to a workflow. Drag-n-Drop AI agents within workflow, directed connections with conditional logic between AI agents.
  2. The skill library should include pydantic classes for defined output of each skill in a classes.py file
  3. Build an interactive simulation widget demonstrating the drag-and-drop workflow canvas and conditional execution logic.
  4. Write the FastAPI backend service that binds these skills to Gemini 2.5 Pro function calls using the classes.py schemas.

#4893 Application goal: Development of a reusable, easy to define and update skill library in folder and text file format (including .md, .json and .py). Easy selection of skills to assign to AI agents. Easy selection of AI agents to apply to a workflow. Drag-n-Drop AI agents within workflow, directed connections with conditional logic between AI agents.

A modular agent architecture that cleanly decouples skills, agents, and directed workflow execution requires three core layers: a self-contained file system representation, an execution engine capable of condition evaluation, and a visual node-edge canvas schema.

1. Folder & File-Based Architecture

A file-driven layout keeps skills human-readable, version-controllable (Git), and dynamically loadable without database overhead:

ai_engine/
├── library/
│   ├── skills/
│   │   ├── system_decomposition/
│   │   │   ├── SKILL.md             # Frontmatter trigger + procedural steps
│   │   │   ├── schema.json          # Input / Output Pydantic contract
│   │   │   └── executor.py          # Python execution logic (tools / transforms)
│   │   └── hazard_verifier/
│   │       ├── SKILL.md
│   │       ├── schema.json
│   │       └── executor.py
│   ├── agents/
│   │   ├── safety_analyst.json      # Assigned skills, system prompt, model params
│   │   └── systems_engineer.json
│   └── workflows/
│       └── hazard_review_flow.json  # Node-edge DAG with conditional routing
├── engine/
│   ├── loaders.py                   # Dynamic filesystem parser (.md, .json, .py)
│   ├── agent_runner.py              # Agent loop & tool dispatcher
│   └── dag_executor.py              # Graph traversal & condition evaluator
└── frontend/                        # Drag-and-drop workflow canvas (React Flow / Svelte Flow)

2. Standardized File Contracts

A. Reusable Skill Definition

Each skill folder encapsulates metadata, prompt instructions, input/output schemas, and deterministic execution tools:

library/skills/hazard_verifier/SKILL.md

---
name: hazard_verifier
version: 1.0.0
description: Validates subsystem hazard causes against design requirements and safety criteria.
tags: [safety, compliance, verification]
---
# Hazard Verifier

Evaluate the target subsystem against provided failure modes.
1. Parse the input event and linked hazard IDs.
2. Verify causal propagation paths.
3. Emit a structured verification score (0.0 to 1.0) and justification.

library/skills/hazard_verifier/schema.json

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "name": "hazard_verifier_io",
  "inputs": {
    "subsystem": {"type": "string"},
    "failure_mode": {"type": "string"},
    "safety_standard": {"type": "string", "default": "NASA-STD-8739.8B"}
  },
  "outputs": {
    "compliant": {"type": "boolean"},
    "risk_score": {"type": "number"},
    "mitigation_notes": {"type": "string"}
  }
}

library/skills/hazard_verifier/executor.py

"""Deterministic helper or external tool invoked by the agent or runner."""
from typing import Dict, Any

def execute(inputs: Dict[str, Any]) -> Dict[str, Any]:
    subsystem = inputs.get("subsystem", "")
    failure_mode = inputs.get("failure_mode", "")
    
    # Deterministic checks or external API lookups
    return {
        "verified_target": f"{subsystem}:{failure_mode}",
        "static_flags": []
    }

B. Agent Definition

Agents reference skills from the library by folder name, binding a system persona and LLM configuration to a specific toolset:

library/agents/safety_analyst.json

{
  "agent_id": "safety_analyst",
  "name": "Safety & Assurance Analyst",
  "model": "gemini-2.5-pro",
  "temperature": 0.2,
  "system_instruction": "You are a software safety specialist evaluating system hazards and process integrity.",
  "skills": [
    "hazard_verifier",
    "system_decomposition"
  ],
  "max_iterations": 5
}

C. Workflow DAG Schema (Drag-n-Drop & Conditional Routing)

Visual canvas systems (e.g., React Flow) operate on nodes and edges. By augmenting edges with boolean conditional expressions, the graph handles dynamic branching:

library/workflows/hazard_review_flow.json

{
  "workflow_id": "flow_hazard_triage",
  "name": "Subsystem Hazard Review Flow",
  "nodes": [
    {
      "id": "node_ingest",
      "type": "agent_node",
      "agent_id": "systems_engineer",
      "position": { "x": 100, "y": 200 },
      "inputs": { "raw_report": "{{workflow.input.report_text}}" }
    },
    {
      "id": "node_safety_check",
      "type": "agent_node",
      "agent_id": "safety_analyst",
      "position": { "x": 450, "y": 200 },
      "inputs": {
        "subsystem": "{{node_ingest.output.subsystem}}",
        "failure_mode": "{{node_ingest.output.failure_mode}}"
      }
    },
    {
      "id": "node_escalation",
      "type": "agent_node",
      "agent_id": "safety_analyst",
      "position": { "x": 800, "y": 100 },
      "inputs": { "alert_level": "CRITICAL", "details": "{{node_safety_check.output}}" }
    },
    {
      "id": "node_approve",
      "type": "agent_node",
      "agent_id": "systems_engineer",
      "position": { "x": 800, "y": 300 },
      "inputs": { "status": "APPROVED", "details": "{{node_safety_check.output}}" }
    }
  ],
  "edges": [
    {
      "id": "e_ingest_to_check",
      "source": "node_ingest",
      "target": "node_safety_check",
      "condition": null
    },
    {
      "id": "e_check_to_escalate",
      "source": "node_safety_check",
      "target": "node_escalation",
      "condition": "node_safety_check.output.risk_score > 0.7 || node_safety_check.output.compliant == false"
    },
    {
      "id": "e_check_to_approve",
      "source": "node_safety_check",
      "target": "node_approve",
      "condition": "node_safety_check.output.risk_score <= 0.7 && node_safety_check.output.compliant == true"
    }
  ]
}

3. Core Python Loader & Graph Traversal Engine

The following implementation scans the folder structure, loads skills dynamically into agents, and runs conditional directed edge execution:

import os
import json
import re
import importlib.util
from typing import Dict, Any, List

class SkillLibrary:
    def __init__(self, base_dir: str):
        self.skills_dir = os.path.join(base_dir, "skills")
        self.skills: Dict[str, Dict[str, Any]] = {}
        self._load_all_skills()

    def _load_all_skills(self):
        if not os.path.exists(self.skills_dir):
            return
        for skill_name in os.listdir(self.skills_dir):
            skill_path = os.path.join(self.skills_dir, skill_name)
            if not os.path.isdir(skill_path):
                continue
            
            # Read SKILL.md
            md_path = os.path.join(skill_path, "SKILL.md")
            instructions, meta = "", {}
            if os.path.exists(md_path):
                with open(md_path, "r", encoding="utf-8") as f:
                    content = f.read()
                    match = re.match(r"^---\n(.*?)\n---\n(.*)$", content, re.DOTALL)
                    if match:
                        raw_meta, instructions = match.groups()
                        for line in raw_meta.split("\n"):
                            if ":" in line:
                                k, v = line.split(":", 1)
                                meta[k.strip()] = v.strip()
                    else:
                        instructions = content

            # Read schema.json
            schema_path = os.path.join(skill_path, "schema.json")
            schema = {}
            if os.path.exists(schema_path):
                with open(schema_path, "r", encoding="utf-8") as f:
                    schema = json.load(f)

            # Dynamically import executor.py if present
            executor_path = os.path.join(skill_path, "executor.py")
            executor_func = None
            if os.path.exists(executor_path):
                spec = importlib.util.spec_from_file_location(f"{skill_name}.executor", executor_path)
                mod = importlib.util.module_from_spec(spec)
                spec.loader.exec_module(mod)
                executor_func = getattr(mod, "execute", None)

            self.skills[skill_name] = {
                "metadata": meta,
                "instructions": instructions.strip(),
                "schema": schema,
                "executor": executor_func
            }


class WorkflowRunner:
    def __init__(self, library: SkillLibrary, agents_dir: str):
        self.library = library
        self.agents_dir = agents_dir

    def load_agent(self, agent_id: str) -> Dict[str, Any]:
        agent_file = os.path.join(self.agents_dir, f"{agent_id}.json")
        with open(agent_file, "r", encoding="utf-8") as f:
            agent_cfg = json.load(f)
        
        # Attach resolved skill objects
        agent_cfg["resolved_skills"] = {
            s: self.library.skills[s] for s in agent_cfg.get("skills", []) if s in self.library.skills
        }
        return agent_cfg

    def evaluate_condition(self, condition: str, context: Dict[str, Any]) -> bool:
        """Evaluates conditional logic expressions safely using a context dict."""
        if not condition:
            return True
        # Simple sandboxed evaluation supporting dot notation variables
        # Ex: "node_safety_check.output.risk_score > 0.7"
        safe_env = {"__builtins__": {}}
        for node_id, data in context.items():
            safe_env[node_id] = data
        try:
            return bool(eval(condition, safe_env))
        except Exception:
            return False

    def execute_workflow(self, workflow_data: Dict[str, Any], initial_input: Dict[str, Any]) -> Dict[str, Any]:
        nodes = {n["id"]: n for n in workflow_data["nodes"]}
        edges = workflow_data["edges"]
        
        # Build adjacency and in-degree tables
        adjacency = {nid: [] for nid in nodes}
        in_degree = {nid: 0 for nid in nodes}
        for edge in edges:
            adjacency[edge["source"]].append(edge)
            in_degree[edge["target"]] += 1

        queue = [nid for nid, deg in in_degree.items() if deg == 0]
        context = {"workflow": {"input": initial_input}}

        while queue:
            curr_id = queue.pop(0)
            curr_node = nodes[curr_id]
            agent = self.load_agent(curr_node["agent_id"])

            # Resolve template inputs (e.g., {{workflow.input.report_text}})
            resolved_inputs = {}
            for k, val in curr_node.get("inputs", {}).items():
                if isinstance(val, str) and "{{" in val:
                    keypath = val.replace("{{", "").replace("}}", "").strip().split(".")
                    cur = context
                    for p in keypath:
                        cur = cur.get(p, {}) if isinstance(cur, dict) else None
                    resolved_inputs[k] = cur
                else:
                    resolved_inputs[k] = val

            # Mock execution or Agent invocation (e.g. calling model with skills injected)
            output = {"status": "executed", "mock_value": 42}
            if curr_id == "node_safety_check":
                output = {"compliant": True, "risk_score": 0.35}

            context[curr_id] = {"output": output}

            # Traverse outgoing edges matching condition
            for edge in adjacency[curr_id]:
                if self.evaluate_condition(edge.get("condition"), context):
                    queue.append(edge["target"])

        return context

4. Implementation Steps for the Visual Canvas

  1. Canvas Engine: Use React Flow or Svelte Flow for the UI. Define a custom AgentNode component displaying:

    • Agent title, avatar, and assigned model.

    • List of bound skill chips (loaded from /library/skills/).

    • Dynamic input/output port sockets mapped to the agent's schema.

  2. Conditional Edge Overlay: Custom edge components render an inline popover allowing users to write condition rules (e.g., risk_score > 0.7) or pick standard comparison operators (==, !=, >, <) based on the upstream node's output schema.

  3. Serialization: The drag-and-drop actions update state in memory and save directly as clean, human-readable JSON files into library/workflows/, matching the file-based schema.


The skill library should include pydantic classes for defined output of each skill in a classes.py file

Integrating a dedicated classes.py file into each skill directory establishes a strict, type-safe data contract. By standardizing on Pydantic BaseModel classes, the system guarantees runtime input/output validation, generates JSON Schemas on demand for tool calling or LLM structured output, and enforces clean boundary types across directed workflow edges.

1. Updated Skill Folder Structure

Each skill folder contains its markdown instructions, functional script logic, and Pydantic schema classes:

library/
└── skills/
    └── hazard_verifier/
        ├── SKILL.md        # Frontmatter, operational guide, few-shot examples
        ├── classes.py      # Pydantic models for Input, Output, & Sub-schemas
        └── executor.py     # Deterministic transform, external APIs, or tool runner

2. Standardized classes.py Pattern

Each classes.py exposes:

  1. An Input schema (<SkillName>Input)

  2. An Output schema (<SkillName>Output)

  3. Helper methods to export standard OpenAI / Gemini tool call definitions (model_json_schema())

library/skills/hazard_verifier/classes.py

from pydantic import BaseModel, Field
from typing import List, Optional
from enum import Enum

class SeverityLevel(str, Enum):
    CATASTROPHIC = "Catastrophic"
    CRITICAL = "Critical"
    MODERATE = "Moderate"
    NEGLIGIBLE = "Negligible"

class FailureMode(BaseModel):
    failure_id: str = Field(..., description="Unique identifier for the failure mode")
    description: str = Field(..., description="Description of the failure sequence")
    severity: SeverityLevel = Field(..., description="NASA hazard classification level")

class HazardVerifierInput(BaseModel):
    subsystem: str = Field(..., description="Target system or subsystem under review")
    failure_modes: List[FailureMode] = Field(..., description="List of failure modes to evaluate")
    safety_standard: str = Field(default="NASA-STD-8739.8B", description="Reference standard")

class HazardVerifierOutput(BaseModel):
    compliant: bool = Field(..., description="True if subsystem passes all safety gates")
    risk_score: float = Field(..., ge=0.0, le=1.0, description="Normalized risk score between 0.0 and 1.0")
    unmitigated_hazards: List[str] = Field(default_factory=list, description="IDs of hazards lacking controls")
    mitigation_notes: str = Field(..., description="Engineering rationale and recommended remediations")

3. Binding classes.py in executor.py

The execution logic validates inputs and returns the structured output model:

library/skills/hazard_verifier/executor.py

from typing import Dict, Any
from .classes import HazardVerifierInput, HazardVerifierOutput

def execute(raw_inputs: Dict[str, Any]) -> HazardVerifierOutput:
    # 1. Enforce strict input validation
    parsed_input = HazardVerifierInput.model_validate(raw_inputs)
    
    # 2. Business logic / tool evaluation
    unmitigated = [
        fm.failure_id for fm in parsed_input.failure_modes 
        if fm.severity in ["Catastrophic", "Critical"]
    ]
    is_compliant = len(unmitigated) == 0
    calculated_risk = 0.85 if not is_compliant else 0.15

    # 3. Return structured Pydantic output
    return HazardVerifierOutput(
        compliant=is_compliant,
        risk_score=calculated_risk,
        unmitigated_hazards=unmitigated,
        mitigation_notes="Automatic verification against " + parsed_input.safety_standard
    )

4. Dynamic Class Loader & Schema Introspection

The engine imports classes.py via importlib.util without hardcoding module imports. It introspects any subclasses of BaseModel to extract the Input/Output types dynamically:

engine/loaders.py

import os
import re
import importlib.util
from typing import Dict, Any, Type, Optional
from pydantic import BaseModel

class SkillMetadata(BaseModel):
    name: str
    version: str = "1.0.0"
    description: str
    tags: list[str] = []

class SkillPackage:
    def __init__(
        self,
        name: str,
        meta: SkillMetadata,
        instructions: str,
        input_class: Optional[Type[BaseModel]],
        output_class: Optional[Type[BaseModel]],
        executor: Any
    ):
        self.name = name
        self.meta = meta
        self.instructions = instructions
        self.input_class = input_class
        self.output_class = output_class
        self.executor = executor

    def get_output_schema_json(self) -> Dict[str, Any]:
        """Provides the JSON schema used by UI canvas ports and LLM structured generation."""
        if self.output_class:
            return self.output_class.model_json_schema()
        return {}


def load_skill_from_folder(skill_dir: str) -> SkillPackage:
    skill_name = os.path.basename(skill_dir.rstrip("/\\"))
    
    # 1. Parse SKILL.md Frontmatter & Body
    md_file = os.path.join(skill_dir, "SKILL.md")
    instructions = ""
    meta_dict = {"name": skill_name, "description": ""}
    if os.path.exists(md_file):
        with open(md_file, "r", encoding="utf-8") as f:
            content = f.read()
            match = re.match(r"^---\n(.*?)\n---\n(.*)$", content, re.DOTALL)
            if match:
                raw_meta, instructions = match.groups()
                for line in raw_meta.split("\n"):
                    if ":" in line:
                        k, v = line.split(":", 1)
                        meta_dict[k.strip()] = v.strip()
            else:
                instructions = content
    
    metadata = SkillMetadata(**meta_dict)

    # 2. Dynamically import classes.py
    classes_file = os.path.join(skill_dir, "classes.py")
    input_cls = None
    output_cls = None
    
    if os.path.exists(classes_file):
        spec = importlib.util.spec_from_file_location(f"{skill_name}.classes", classes_file)
        classes_mod = importlib.util.module_from_spec(spec)
        spec.loader.exec_module(classes_mod)
        
        # Discover models matching naming conventions or BaseModel subclasses
        for attr_name in dir(classes_mod):
            attr = getattr(classes_mod, attr_name)
            if isinstance(attr, type) and issubclass(attr, BaseModel) and attr is not BaseModel:
                if "Input" in attr_name:
                    input_cls = attr
                elif "Output" in attr_name:
                    output_cls = attr

    # 3. Dynamically import executor.py
    executor_file = os.path.join(skill_dir, "executor.py")
    executor_func = None
    if os.path.exists(executor_file):
        spec_exec = importlib.util.spec_from_file_location(f"{skill_name}.executor", executor_file)
        exec_mod = importlib.util.module_from_spec(spec_exec)
        spec_exec.loader.exec_module(exec_mod)
        executor_func = getattr(exec_mod, "execute", None)

    return SkillPackage(
        name=skill_name,
        meta=metadata,
        instructions=instructions.strip(),
        input_class=input_cls,
        output_class=output_cls,
        executor=executor_func
    )

5. Type-Safe Workflow Edge Verification

When dragging a connection between Agent A and Agent B in the UI, the backend checks the Pydantic schemas before running the workflow:

def validate_edge_compatibility(
    source_skill: SkillPackage, 
    target_skill: SkillPackage
) -> bool:
    """Verifies that target input requirements exist within source output fields."""
    if not source_skill.output_class or not target_skill.input_class:
        return True  # Fallback to loose validation if schemas are unassigned

    source_fields = set(source_skill.output_class.model_fields.keys())
    target_required_fields = {
        name for name, field in target_skill.input_class.model_fields.items()
        if field.is_required()
    }

    # Verify all non-default fields expected downstream exist in upstream output
    missing = target_required_fields - source_fields
    if missing:
        raise ValueError(
            f"Edge validation failed: Target '{target_skill.name}' requires fields {missing} "
            f"missing from source '{source_skill.name}' output."
        )
    return True


Build an interactive simulation widget demonstrating the drag-and-drop workflow canvas and conditional execution logic.

I will now generate an interactive, self-contained single-page simulation application that implements the skill library, agent palette, visual node-edge drag-and-drop canvas, conditional branching inspector, and step-by-step workflow execution engine with live variable tracing.


Key Highlights of the Application

  1. File-Based Skill Directory & Inspection Drawer:

    • Folders for skills such as hazard_verifier, system_decomposition, code_linter, and sentiment_evaluator.

    • Clicking Inspect exposes the three critical skill files in a tabbed IDE modal: SKILL.md (frontmatter + procedural rules), classes.py (Pydantic Input/Output models), and executor.py.

  2. Visual Drag-and-Drop Agent Canvas:

    • Draggable agent cards from the left palette (Systems Engineer, Safety Analyst, QA Gatekeeper, Escalation Manager) drop directly onto the infinite grid canvas.

    • Nodes render with model chips, bound skill badges, input/output connection sockets, and live execution status rings.

  3. Directed Edges & AST Condition Evaluation:

    • Output sockets drag to input sockets to establish directed connections.

    • Clicking any connection path or rule badge opens the Edge Condition Editor, allowing rules like risk_score > 0.7 or severity == 'Catastrophic' to be configured with presets or arbitrary boolean expressions.

  4. Step-by-Step Simulation & Variable Context:

    • Clicking Run Simulation steps through the directed graph with speed controls (0.5x, 1.0x, 2.0x).

    • Downstream edge conditions evaluate against upstream output variables in real time. Matching paths pulse in emerald green (TRUE), while pruned paths dim in dashed red (FALSE).

    • The right drawer displays the live Pydantic Variable Context and event timeline.


Write the FastAPI backend service that binds these skills to Gemini 2.5 Pro function calls using the classes.py schemas.

The FastAPI backend service below dynamically loads file-based skills (SKILL.md, classes.py, executor.py), translates Pydantic schemas into Google GenAI function declarations, runs agent tool calls against Gemini 2.5 Pro, and executes the directed workflow DAG with edge conditions.

Project Layout

backend/
├── app.py                   # FastAPI service, Gemini client & workflow endpoints
├── library_manager.py       # Dynamic skill and agent disk loader
├── condition_evaluator.py   # Safe AST condition parser for edge logic
└── library/
    ├── skills/
    │   ├── hazard_verifier/
    │   │   ├── SKILL.md
    │   │   ├── classes.py
    │   │   └── executor.py
    │   └── system_decomposition/
    │       ├── SKILL.md
    │       ├── classes.py
    │       └── executor.py
    ├── agents/
    │   └── safety_analyst.json
    └── workflows/
        └── hazard_review_flow.json

1. backend/condition_evaluator.py (Safe AST Edge Routing)

Replaces unsafe eval() calls with a safe, restricted AST walker supporting comparisons and logical operators (&&, ||, >, <, ==, !=):

import ast
import operator
from typing import Any, Dict

SAFE_OPERATORS = {
    ast.Eq: operator.eq,
    ast.NotEq: operator.ne,
    ast.Lt: operator.lt,
    ast.LtE: operator.le,
    ast.Gt: operator.gt,
    ast.GtE: operator.ge,
    ast.And: lambda a, b: a and b,
    ast.Or: lambda a, b: a or b,
    ast.Not: operator.not_,
}

def _resolve_attr(node: ast.AST, context: Dict[str, Any]) -> Any:
    if isinstance(node, ast.Name):
        return context.get(node.id)
    if isinstance(node, ast.Attribute):
        parent = _resolve_attr(node.value, context)
        if isinstance(parent, dict):
            return parent.get(node.attr)
        return getattr(parent, node.attr, None)
    if isinstance(node, ast.Constant):
        return node.value
    raise ValueError(f"Unsupported AST node in expression: {type(node)}")

def evaluate_edge_condition(expression: str, context: Dict[str, Any]) -> bool:
    """Safely evaluates an edge rule against output state without using eval()."""
    if not expression or not expression.strip():
        return True
    
    # Normalize standard JS boolean operators to Python syntax
    normalized = expression.replace("&&", " and ").replace("||", " or ")
    tree = ast.parse(normalized, mode="eval")

    def _eval(node: ast.AST) -> Any:
        if isinstance(node, ast.Expression):
            return _eval(node.body)
        elif isinstance(node, (ast.Name, ast.Attribute, ast.Constant)):
            return _resolve_attr(node, context)
        elif isinstance(node, ast.Compare):
            left = _eval(node.left)
            for op, comparator in zip(node.ops, node.comparators):
                right = _eval(comparator)
                op_fn = SAFE_OPERATORS.get(type(op))
                if not op_fn or not op_fn(left, right):
                    return False
            return True
        elif isinstance(node, ast.BoolOp):
            if isinstance(node.op, ast.And):
                return all(_eval(val) for val in node.values)
            elif isinstance(node.op, ast.Or):
                return any(_eval(val) for val in node.values)
        elif isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.Not):
            return not _eval(node.operand)
        raise ValueError(f"Disallowed expression node: {type(node)}")

    try:
        return bool(_eval(tree))
    except Exception:
        return False

2. backend/library_manager.py (Dynamic Schema & File Loader)

Parses classes.py, inspects Pydantic types, extracts JSON schemas for Gemini tool declarations, and loads executable tool call targets:

import os
import re
import importlib.util
from typing import Dict, Any, Type, Optional
from pydantic import BaseModel

class SkillPackage:
    def __init__(
        self,
        name: str,
        description: str,
        instructions: str,
        input_class: Optional[Type[BaseModel]],
        output_class: Optional[Type[BaseModel]],
        executor: Any
    ):
        self.name = name
        self.description = description
        self.instructions = instructions
        self.input_class = input_class
        self.output_class = output_class
        self.executor = executor

    def to_gemini_tool_declaration(self) -> Dict[str, Any]:
        """Converts Pydantic input schema into a Gemini function declaration dictionary."""
        parameters = self.input_class.model_json_schema() if self.input_class else {"type": "object", "properties": {}}
        # Clean title & $defs for compatibility with Gemini tool specifications
        parameters.pop("title", None)
        return {
            "name": self.name,
            "description": self.description,
            "parameters": parameters
        }

class LibraryManager:
    def __init__(self, base_dir: str):
        self.base_dir = base_dir
        self.skills: Dict[str, SkillPackage] = {}
        self.reload_all_skills()

    def reload_all_skills(self):
        skills_path = os.path.join(self.base_dir, "skills")
        if not os.path.exists(skills_path):
            return

        for skill_dir in os.listdir(skills_path):
            full_path = os.path.join(skills_path, skill_dir)
            if not os.path.isdir(full_path):
                continue
            
            # 1. Parse SKILL.md
            md_file = os.path.join(full_path, "SKILL.md")
            instructions = ""
            desc = f"Executes {skill_dir}"
            if os.path.exists(md_file):
                with open(md_file, "r", encoding="utf-8") as f:
                    content = f.read()
                    match = re.match(r"^---\n(.*?)\n---\n(.*)$", content, re.DOTALL)
                    if match:
                        raw_meta, instructions = match.groups()
                        for line in raw_meta.split("\n"):
                            if line.startswith("description:"):
                                desc = line.split(":", 1)[1].strip()
                    else:
                        instructions = content

            # 2. Parse classes.py
            classes_file = os.path.join(full_path, "classes.py")
            input_cls, output_cls = None, None
            if os.path.exists(classes_file):
                spec = importlib.util.spec_from_file_location(f"skills.{skill_dir}.classes", classes_file)
                mod = importlib.util.module_from_spec(spec)
                spec.loader.exec_module(mod)
                for attr_name in dir(mod):
                    attr = getattr(mod, attr_name)
                    if isinstance(attr, type) and issubclass(attr, BaseModel) and attr is not BaseModel:
                        if "Input" in attr_name:
                            input_cls = attr
                        elif "Output" in attr_name:
                            output_cls = attr

            # 3. Parse executor.py
            executor_file = os.path.join(full_path, "executor.py")
            executor_fn = None
            if os.path.exists(executor_file):
                spec_exec = importlib.util.spec_from_file_location(f"skills.{skill_dir}.executor", executor_file)
                mod_exec = importlib.util.module_from_spec(spec_exec)
                spec_exec.loader.exec_module(mod_exec)
                executor_fn = getattr(mod_exec, "execute", None)

            self.skills[skill_dir] = SkillPackage(
                name=skill_dir,
                description=desc,
                instructions=instructions.strip(),
                input_class=input_cls,
                output_class=output_cls,
                executor=executor_fn
            )

3. backend/app.py (FastAPI Server with Gemini 2.5 Pro Tool Calling)

Integrates the official google-genai SDK with function call handling via types.Part.from_function_response, executes deterministic tools, and drives graph DAG routing:

import os
import json
from typing import Dict, Any, List, Optional
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, Field
from google import genai
from google.genai import types

from library_manager import LibraryManager
from condition_evaluator import evaluate_edge_condition

app = FastAPI(title="Gemini Agent Workflow Engine", version="1.0.0")

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

LIBRARY_DIR = os.path.join(os.path.dirname(__file__), "library")
manager = LibraryManager(LIBRARY_DIR)
client = genai.Client()  # Picks up GEMINI_API_KEY from environment

# Request / Response Schemas
class ExecuteAgentRequest(BaseModel):
    agent_id: str
    task_prompt: str
    input_data: Dict[str, Any] = Field(default_factory=dict)

class WorkflowNode(BaseModel):
    id: str
    agent_id: str
    inputs: Dict[str, Any] = Field(default_factory=dict)

class WorkflowEdge(BaseModel):
    id: str
    source: str
    target: str
    condition: Optional[str] = None

class WorkflowRunRequest(BaseModel):
    workflow_id: str
    nodes: List[WorkflowNode]
    edges: List[WorkflowEdge]
    initial_input: Dict[str, Any]


@app.get("/api/skills")
def list_skills():
    """Returns all available skills, their metadata, and Pydantic schemas."""
    manager.reload_all_skills()
    results = {}
    for name, skill in manager.skills.items():
        results[name] = {
            "name": skill.name,
            "description": skill.description,
            "input_schema": skill.input_class.model_json_schema() if skill.input_class else {},
            "output_schema": skill.output_class.model_json_schema() if skill.output_class else {}
        }
    return results


async def run_agent_loop(agent_config: Dict[str, Any], prompt: str) -> Dict[str, Any]:
    """Autonomous Gemini 2.5 Pro tool-calling refinement loop."""
    assigned_skills = [
        manager.skills[name] for name in agent_config.get("skills", [])
        if name in manager.skills
    ]

    function_declarations = [s.to_gemini_tool_declaration() for s in assigned_skills]
    gemini_tools = [{"function_declarations": function_declarations}] if function_declarations else None

    system_instruction = agent_config.get("system_instruction", "You are an expert AI assistant.")
    for s in assigned_skills:
        system_instruction += f"\n\n### Available Skill: {s.name}\n{s.instructions}"

    config = types.GenerateContentConfig(
        system_instruction=system_instruction,
        temperature=agent_config.get("temperature", 0.2),
        tools=gemini_tools
    )

    contents: List[Any] = [prompt]
    max_turns = agent_config.get("max_iterations", 5)

    for _ in range(max_turns):
        response = client.models.generate_content(
            model=agent_config.get("model", "gemini-2.5-pro"),
            contents=contents,
            config=config
        )

        # Check if the model triggered any tool calls
        candidate = response.candidates[0]
        has_tool_call = False
        tool_call_parts = []

        for part in candidate.content.parts:
            if part.function_call:
                has_tool_call = True
                call = part.function_call
                skill = manager.skills.get(call.name)
                
                if not skill or not skill.executor:
                    result_payload = {"error": f"Executor for {call.name} not implemented."}
                else:
                    try:
                        # 1. Validate payload against Pydantic Input model
                        raw_args = dict(call.args)
                        if skill.input_class:
                            validated_input = skill.input_class.model_validate(raw_args)
                            execution_result = skill.executor(validated_input.model_dump())
                        else:
                            execution_result = skill.executor(raw_args)

                        # 2. Serialize output model if applicable
                        if isinstance(execution_result, BaseModel):
                            result_payload = execution_result.model_dump()
                        else:
                            result_payload = execution_result
                    except Exception as e:
                        result_payload = {"validation_or_execution_error": str(e)}

                tool_call_parts.append(types.Part.from_function_response(
                    name=call.name,
                    response={"result": result_payload}
                ))

        if not has_tool_call:
            # Model emitted final response
            return {"text": response.text, "structured": {}}

        # Append candidate response and tool responses back into the conversation context
        contents.append(candidate.content)
        contents.append(types.Content(parts=tool_call_parts))

    return {"text": response.text, "status": "max_iterations_reached"}


@app.post("/api/agent/execute")
async def execute_agent_endpoint(req: ExecuteAgentRequest):
    agent_file = os.path.join(LIBRARY_DIR, "agents", f"{req.agent_id}.json")
    if not os.path.exists(agent_file):
        raise HTTPException(status_code=404, detail="Agent profile not found")

    with open(agent_file, "r", encoding="utf-8") as f:
        agent_config = json.load(f)

    prompt = f"User Request: {req.task_prompt}\nContext Data: {json.dumps(req.input_data)}"
    result = await run_agent_loop(agent_config, prompt)
    return {"agent_id": req.agent_id, "output": result}


@app.post("/api/workflow/run")
async def run_workflow(req: WorkflowRunRequest):
    """Executes a directed graph canvas payload with edge condition gating."""
    nodes = {n.id: n for n in req.nodes}
    adjacency: Dict[str, List[WorkflowEdge]] = {n.id: [] for n in req.nodes}
    in_degree: Dict[str, int] = {n.id: 0 for n in req.nodes}

    for edge in req.edges:
        adjacency[edge.source].append(edge)
        in_degree[edge.target] += 1

    queue = [nid for nid, deg in in_degree.items() if deg == 0]
    workflow_context: Dict[str, Any] = {"workflow": {"input": req.initial_input}}
    execution_trace: List[Dict[str, Any]] = []

    while queue:
        curr_id = queue.pop(0)
        curr_node = nodes[curr_id]

        agent_file = os.path.join(LIBRARY_DIR, "agents", f"{curr_node.agent_id}.json")
        if not os.path.exists(agent_file):
            raise HTTPException(status_code=404, detail=f"Agent '{curr_node.agent_id}' missing.")

        with open(agent_file, "r", encoding="utf-8") as f:
            agent_config = json.load(f)

        # Dynamic variable templating: replace {{node.output.field}}
        resolved_inputs = {}
        for k, v in curr_node.inputs.items():
            if isinstance(v, str) and "{{" in v:
                path = v.replace("{{", "").replace("}}", "").strip().split(".")
                cur = workflow_context
                for part in path:
                    cur = cur.get(part, {}) if isinstance(cur, dict) else None
                resolved_inputs[k] = cur
            else:
                resolved_inputs[k] = v

        prompt = f"Process input: {json.dumps(resolved_inputs)}"
        result = await run_agent_loop(agent_config, prompt)

        # Record node output into context
        workflow_context[curr_id] = {"output": result}
        execution_trace.append({"node_id": curr_id, "output": result})

        # Evaluate outgoing edges
        for edge in adjacency[curr_id]:
            passed = evaluate_edge_condition(edge.condition, workflow_context)
            if passed:
                queue.append(edge.target)

    return {
        "workflow_id": req.workflow_id,
        "trace": execution_trace,
        "final_context": workflow_context
    }

if __name__ == "__main__":
    import uvicorn
    uvicorn.run("app:app", host="0.0.0.0", port=8000, reload=True)

4. Running and Testing the Service

  1. Install dependencies:

    pip install fastapi uvicorn google-genai pydantic
    
  2. Export your Gemini API Key:

    export GEMINI_API_KEY="your-api-key"
    
  3. Start the backend server:

    python app.py
    
  4. Trigger an Agent execution:

    curl -X POST "http://localhost:8000/api/agent/execute" \
      -H "Content-Type: application/json" \
      -d '{
        "agent_id": "safety_analyst",
        "task_prompt": "Verify RCS thruster valve failure against NASA standards.",
        "input_data": {
          "subsystem": "Propulsion",
          "failure_modes": [
            {"failure_id": "FM-01", "description": "Sol-valve stuck open", "severity": "Catastrophic"}
          ]
        }
      }'


From <https://gemini.google.com/app/e0fe751adbe1cd48>  Google Gemini (3.8 Flash)



<!DOCTYPE html>

<html lang="en">

<head>

  <meta charset="UTF-8">

  <meta name="viewport" content="width=device-width, initial-scale=1.0">

  <title>Agent Workflow Canvas & Skill Library</title>

  <script src="https://cdn.tailwindcss.com"></script>

  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">

  <style>

    /* Custom Scrollbars */

    ::-webkit-scrollbar { width: 6px; height: 6px; }

    ::-webkit-scrollbar-track { background: #0f172a; }

    ::-webkit-scrollbar-thumb { background: #334155; border-radius: 3px; }

    ::-webkit-scrollbar-thumb:hover { background: #475569; }


    /* Canvas Grid */

    .canvas-grid {

      background-size: 24px 24px;

      background-image: 

        radial-gradient(circle, rgba(255, 255, 255, 0.07) 1px, transparent 1px);

    }


    /* Edge Animation */

    @keyframes pulse-flow {

      to {

        stroke-dashoffset: -24;

      }

    }

    .edge-active {

      stroke: #38bdf8 !important;

      stroke-width: 3px !important;

      stroke-dasharray: 6, 6;

      animation: pulse-flow 0.8s linear infinite;

      filter: drop-shadow(0 0 6px rgba(56, 189, 248, 0.8));

    }

    .edge-true {

      stroke: #10b981 !important;

      stroke-width: 3px !important;

      filter: drop-shadow(0 0 6px rgba(16, 185, 129, 0.6));

    }

    .edge-false {

      stroke: #ef4444 !important;

      stroke-width: 2px !important;

      stroke-dasharray: 4, 4;

      opacity: 0.5;

    }


    /* Node glow states */

    .node-running {

      border-color: #38bdf8 !important;

      box-shadow: 0 0 20px rgba(56, 189, 248, 0.35);

    }

    .node-passed {

      border-color: #10b981 !important;

      box-shadow: 0 0 15px rgba(16, 185, 129, 0.25);

    }

    .node-branched {

      border-color: #f59e0b !important;

    }

    .node-skipped {

      opacity: 0.45;

      border-style: dashed;

    }


    /* Port hover */

    .handle-port:hover {

      transform: scale(1.35);

      box-shadow: 0 0 8px #38bdf8;

    }

  </style>

</head>

<body class="bg-slate-950 text-slate-100 flex flex-col h-screen w-screen overflow-hidden font-sans select-none">


  <!-- TOP HEADER & CONTROLS -->

  <header class="h-14 bg-slate-900 border-b border-slate-800 px-4 flex items-center justify-between z-30">

    <div class="flex items-center gap-3">

      <div class="w-9 h-9 rounded-lg bg-gradient-to-tr from-cyan-600 to-indigo-600 flex items-center justify-center shadow-lg shadow-indigo-500/20 text-white font-bold text-lg">

        <i class="fa-solid fa-network-wired text-sm"></i>

      </div>

      <div>

        <h1 class="text-sm font-semibold tracking-wide flex items-center gap-2">

          <span>AgentFlow Studio</span>

          <span class="text-[10px] uppercase font-mono px-2 py-0.5 rounded bg-cyan-950 text-cyan-400 border border-cyan-800">Skill Library & DAG Engine</span>

        </h1>

        <p class="text-[11px] text-slate-400">Pydantic Schema Contracts • Directed Graph • AST Conditional Transitions</p>

      </div>

    </div>


    <!-- Actions -->

    <div class="flex items-center gap-2">

      <div class="flex items-center bg-slate-950 border border-slate-800 rounded-lg p-1 text-xs">

        <span class="text-slate-400 px-2 text-[11px] font-mono">Speed:</span>

        <button id="speed-slow" class="px-2 py-1 rounded text-slate-400 hover:text-white transition">0.5x</button>

        <button id="speed-norm" class="px-2 py-1 rounded bg-slate-800 text-cyan-400 font-semibold transition">1.0x</button>

        <button id="speed-fast" class="px-2 py-1 rounded text-slate-400 hover:text-white transition">2.0x</button>

      </div>


      <button id="btn-sample-flow" class="px-3 py-1.5 rounded-lg bg-slate-800 hover:bg-slate-700 text-xs text-slate-200 border border-slate-700 flex items-center gap-1.5 transition">

        <i class="fa-solid fa-folder-open text-cyan-400"></i> Sample Flow

      </button>


      <button id="btn-validate" class="px-3 py-1.5 rounded-lg bg-slate-800 hover:bg-slate-700 text-xs text-slate-200 border border-slate-700 flex items-center gap-1.5 transition">

        <i class="fa-solid fa-shield-halved text-emerald-400"></i> Validate Contracts

      </button>


      <button id="btn-clear" class="px-3 py-1.5 rounded-lg bg-slate-800 hover:bg-slate-700 text-xs text-rose-400 border border-slate-700 flex items-center gap-1.5 transition">

        <i class="fa-solid fa-trash-can"></i> Clear

      </button>


      <button id="btn-run" class="px-4 py-1.5 rounded-lg bg-gradient-to-r from-cyan-600 to-blue-600 hover:from-cyan-500 hover:to-blue-500 text-xs font-semibold text-white shadow-lg shadow-cyan-600/30 flex items-center gap-2 transition active:scale-95">

        <i class="fa-solid fa-play"></i> Run Simulation

      </button>

    </div>

  </header>


  <!-- WORKSPACE BODY (3-COLUMNS / MULTI-PANEL) -->

  <div class="flex-1 flex overflow-hidden relative">


    <!-- LEFT SIDEBAR: SKILL LIBRARY & AGENT PALETTE -->

    <aside class="w-80 bg-slate-900 border-r border-slate-800 flex flex-col z-20 flex-shrink-0">

      

      <!-- Nav Tabs -->

      <div class="flex border-b border-slate-800 bg-slate-900/50">

        <button id="tab-btn-agents" class="flex-1 py-2.5 text-xs font-medium border-b-2 border-cyan-500 text-cyan-400 flex items-center justify-center gap-2">

          <i class="fa-solid fa-robot"></i> Agent Palette

        </button>

        <button id="tab-btn-skills" class="flex-1 py-2.5 text-xs font-medium border-b-2 border-transparent text-slate-400 hover:text-slate-200 flex items-center justify-center gap-2">

          <i class="fa-solid fa-boxes-stacked"></i> Skill Library

        </button>

      </div>


      <!-- Tab Content: Agents -->

      <div id="tab-agents" class="flex-1 overflow-y-auto p-3 space-y-3">

        <div class="text-[11px] text-slate-400 uppercase tracking-wider font-mono flex items-center justify-between">

          <span>Draggable Agent Templates</span>

          <span class="text-slate-500">Drag to canvas</span>

        </div>


        <div id="agent-palette-list" class="space-y-2.5">

          <!-- Rendered dynamically -->

        </div>


        <div class="p-3 rounded-lg bg-slate-950/70 border border-slate-800 text-[11px] text-slate-400">

          <i class="fa-solid fa-circle-info text-cyan-400 mr-1"></i>

          Drag an agent template onto the grid canvas. Each agent encapsulates bound library skills, prompt instructions, and Pydantic schema validation.

        </div>

      </div>


      <!-- Tab Content: Skills Library -->

      <div id="tab-skills" class="flex-1 overflow-y-auto p-3 space-y-2.5 hidden">

        <div class="text-[11px] text-slate-400 uppercase tracking-wider font-mono flex items-center justify-between">

          <span>Skill Directory</span>

          <span class="text-slate-500 text-[10px]">Folder & Text Format</span>

        </div>


        <div id="skill-library-list" class="space-y-2">

          <!-- Rendered dynamically -->

        </div>

      </div>


      <!-- Footer Info -->

      <div class="p-2.5 border-t border-slate-800 bg-slate-950/50 text-[11px] text-slate-400 flex items-center justify-between">

        <span class="flex items-center gap-1.5"><span class="w-2 h-2 rounded-full bg-emerald-400"></span> Engine Ready</span>

        <span class="font-mono text-[10px] text-slate-500">v2.4.0-AST</span>

      </div>

    </aside>


    <!-- CENTER CANVAS -->

    <main id="canvas-container" class="flex-1 relative overflow-hidden canvas-grid bg-slate-950 select-none">

      

      <!-- Interactive SVG Layer for Edges -->

      <svg id="svg-layer" class="absolute inset-0 w-full h-full pointer-events-none z-10">

        <defs>

          <marker id="arrow" viewBox="0 0 10 10" refX="7" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">

            <path d="M 0 1 L 9 5 L 0 9 z" fill="#64748b" />

          </marker>

          <marker id="arrow-active" viewBox="0 0 10 10" refX="7" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">

            <path d="M 0 1 L 9 5 L 0 9 z" fill="#38bdf8" />

          </marker>

          <marker id="arrow-true" viewBox="0 0 10 10" refX="7" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">

            <path d="M 0 1 L 9 5 L 0 9 z" fill="#10b981" />

          </marker>

          <marker id="arrow-false" viewBox="0 0 10 10" refX="7" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">

            <path d="M 0 1 L 9 5 L 0 9 z" fill="#ef4444" />

          </marker>

        </defs>

        <g id="edges-group" class="pointer-events-auto"></g>

        <path id="temp-edge" class="stroke-cyan-400 stroke-2 fill-none stroke-dasharray-4 pointer-events-none hidden" />

      </svg>


      <!-- HTML Nodes Container -->

      <div id="nodes-container" class="absolute inset-0 z-10 pointer-events-none">

        <!-- Rendered Node Elements here -->

      </div>


      <!-- Quick Canvas Overlay Controls -->

      <div class="absolute bottom-4 left-4 z-20 flex items-center gap-2 bg-slate-900/90 backdrop-blur border border-slate-800 rounded-lg p-1.5 shadow-xl text-xs text-slate-300">

        <button id="btn-zoom-in" class="w-7 h-7 flex items-center justify-center hover:bg-slate-800 rounded transition"><i class="fa-solid fa-plus"></i></button>

        <button id="btn-zoom-reset" class="px-2 h-7 flex items-center justify-center hover:bg-slate-800 rounded font-mono text-[11px] transition">100%</button>

        <button id="btn-zoom-out" class="w-7 h-7 flex items-center justify-center hover:bg-slate-800 rounded transition"><i class="fa-solid fa-minus"></i></button>

        <div class="h-4 w-px bg-slate-800"></div>

        <span class="text-[10px] text-slate-400 px-1"><i class="fa-solid fa-circle-nodes text-cyan-400 mr-1"></i>Connect Output ➔ Input ports</span>

      </div>

    </main>


    <!-- RIGHT INSPECTOR / TRACE DRAWER -->

    <aside id="inspector-panel" class="w-96 bg-slate-900 border-l border-slate-800 flex flex-col z-20 flex-shrink-0 transition-all duration-200">

      

      <!-- Panel Header -->

      <div class="h-11 border-b border-slate-800 px-3 flex items-center justify-between bg-slate-900">

        <div class="flex items-center gap-2">

          <i class="fa-solid fa-terminal text-cyan-400 text-xs"></i>

          <span id="inspector-title" class="text-xs font-semibold uppercase tracking-wider text-slate-200">Execution Trace & Variable Context</span>

        </div>

        <div class="flex items-center gap-1.5">

          <button id="btn-clear-trace" class="text-slate-400 hover:text-rose-400 text-xs px-2 py-0.5 transition" title="Clear logs">

            <i class="fa-solid fa-eraser"></i>

          </button>

        </div>

      </div>


      <!-- Panel Mode Tabs -->

      <div class="flex border-b border-slate-800 bg-slate-950/40 text-[11px]">

        <button id="tab-btn-trace" class="flex-1 py-1.5 font-medium border-b-2 border-cyan-500 text-cyan-400">Live Trace & Context</button>

        <button id="tab-btn-nodecfg" class="flex-1 py-1.5 font-medium border-b-2 border-transparent text-slate-400 hover:text-slate-200">Node Properties</button>

      </div>


      <!-- View: Live Trace -->

      <div id="view-trace" class="flex-1 flex flex-col overflow-hidden p-3 gap-3">

        

        <!-- Live AST Evaluator Box -->

        <div class="bg-slate-950 border border-slate-800 rounded-lg p-2.5">

          <div class="flex items-center justify-between mb-1.5">

            <span class="text-[10px] font-mono text-cyan-400 uppercase tracking-wider font-semibold">AST Condition Evaluator</span>

            <span id="ast-eval-badge" class="text-[9px] px-1.5 py-0.5 rounded bg-slate-800 text-slate-400 font-mono">Standby</span>

          </div>

          <div id="ast-preview" class="text-xs font-mono bg-slate-900/80 p-2 rounded border border-slate-800 text-slate-300 break-all min-h-[38px] flex items-center">

            No active edge transition evaluated.

          </div>

        </div>


        <!-- Global Context JSON Tree -->

        <div class="flex-1 flex flex-col min-h-0 bg-slate-950 border border-slate-800 rounded-lg p-2.5">

          <div class="flex items-center justify-between mb-1">

            <span class="text-[10px] font-mono text-indigo-400 uppercase tracking-wider font-semibold">Workflow Memory Context</span>

            <span class="text-[10px] text-slate-500 font-mono">Pydantic Dict</span>

          </div>

          <pre id="context-viewer" class="flex-1 overflow-auto text-[11px] font-mono text-emerald-400 bg-slate-900/60 p-2 rounded border border-slate-800 leading-tight">{}</pre>

        </div>


        <!-- Event Logs -->

        <div class="h-44 flex flex-col min-h-0 bg-slate-950 border border-slate-800 rounded-lg p-2.5">

          <div class="flex items-center justify-between mb-1">

            <span class="text-[10px] font-mono text-amber-400 uppercase tracking-wider font-semibold">DAG Step Timeline</span>

            <span id="log-count" class="text-[9px] font-mono text-slate-500">0 events</span>

          </div>

          <div id="trace-logs" class="flex-1 overflow-y-auto space-y-1 font-mono text-[10px] text-slate-300 pr-1">

            <div class="text-slate-500 italic">Ready for run...</div>

          </div>

        </div>

      </div>


      <!-- View: Node Config / Inspector -->

      <div id="view-nodecfg" class="flex-1 overflow-y-auto p-3 space-y-3 hidden">

        <div id="no-node-selected" class="h-40 flex items-center justify-center text-xs text-slate-500 italic text-center p-4">

          Select a node on the canvas to inspect skills, schemas, and inputs.

        </div>

        <div id="node-config-form" class="space-y-3 hidden">

          <div>

            <label class="text-[10px] uppercase font-mono text-slate-400 block mb-1">Node Identifier</label>

            <input id="cfg-node-id" type="text" readonly class="w-full bg-slate-950 border border-slate-800 rounded p-1.5 text-xs font-mono text-slate-400">

          </div>

          <div>

            <label class="text-[10px] uppercase font-mono text-slate-400 block mb-1">Agent Name</label>

            <input id="cfg-agent-name" type="text" class="w-full bg-slate-950 border border-slate-800 rounded p-1.5 text-xs text-slate-200 focus:border-cyan-500 outline-none">

          </div>

          <div>

            <label class="text-[10px] uppercase font-mono text-slate-400 block mb-1">Foundation Model</label>

            <select id="cfg-agent-model" class="w-full bg-slate-950 border border-slate-800 rounded p-1.5 text-xs text-slate-200 focus:border-cyan-500 outline-none">

              <option value="gemini-2.5-pro">Gemini 2.5 Pro (Analytical)</option>

              <option value="gemini-2.5-flash">Gemini 2.5 Flash (Fast Execution)</option>

            </select>

          </div>

          <div>

            <label class="text-[10px] uppercase font-mono text-slate-400 block mb-1">Assigned Skills</label>

            <div id="cfg-assigned-skills" class="flex flex-wrap gap-1 p-2 bg-slate-950 border border-slate-800 rounded min-h-[44px]"></div>

          </div>

          <div>

            <label class="text-[10px] uppercase font-mono text-slate-400 block mb-1">Simulated Output Override (JSON)</label>

            <textarea id="cfg-mock-output" rows="5" class="w-full bg-slate-950 border border-slate-800 rounded p-2 text-[11px] font-mono text-emerald-300 focus:border-cyan-500 outline-none"></textarea>

            <p class="text-[10px] text-slate-500 mt-1">Directly simulates the Pydantic Output returned by this agent.</p>

          </div>

          <button id="btn-save-nodecfg" class="w-full py-1.5 bg-cyan-600 hover:bg-cyan-500 rounded text-xs font-semibold text-white transition">Save Node Config</button>

        </div>

      </div>

    </aside>

  </div>


  <!-- MODAL: SKILL INSPECTOR DRAWER (SKILL.md, classes.py, executor.py) -->

  <div id="modal-skill-viewer" class="fixed inset-0 bg-black/70 backdrop-blur-sm z-50 flex items-center justify-center hidden p-6">

    <div class="bg-slate-900 border border-slate-800 rounded-xl w-full max-w-4xl max-h-[85vh] flex flex-col shadow-2xl overflow-hidden">

      <!-- Modal Header -->

      <div class="h-14 border-b border-slate-800 px-5 flex items-center justify-between bg-slate-900">

        <div class="flex items-center gap-3">

          <div class="w-8 h-8 rounded-lg bg-cyan-950 border border-cyan-800 flex items-center justify-center text-cyan-400">

            <i class="fa-solid fa-code text-sm"></i>

          </div>

          <div>

            <div class="flex items-center gap-2">

              <h3 id="skill-modal-name" class="font-bold text-sm text-slate-100">hazard_verifier</h3>

              <span id="skill-modal-version" class="text-[10px] font-mono px-2 py-0.5 rounded bg-slate-800 text-slate-400">1.0.0</span>

            </div>

            <p id="skill-modal-desc" class="text-[11px] text-slate-400">Evaluates failure sequences against NASA-STD-8739.8B.</p>

          </div>

        </div>

        <button id="btn-close-skill-modal" class="text-slate-400 hover:text-white text-lg p-1.5"><i class="fa-solid fa-xmark"></i></button>

      </div>


      <!-- File Tabs -->

      <div class="flex border-b border-slate-800 bg-slate-950/70 px-4 gap-2 pt-2 text-xs font-mono">

        <button id="skill-tab-md" class="px-3 py-1.5 rounded-t border-t border-x border-slate-700 bg-slate-900 text-cyan-400 flex items-center gap-1.5">

          <i class="fa-regular fa-file-lines"></i> SKILL.md

        </button>

        <button id="skill-tab-classes" class="px-3 py-1.5 rounded-t border-t border-x border-transparent text-slate-400 hover:text-slate-200 flex items-center gap-1.5">

          <i class="fa-brands fa-python text-yellow-500"></i> classes.py (Pydantic)

        </button>

        <button id="skill-tab-executor" class="px-3 py-1.5 rounded-t border-t border-x border-transparent text-slate-400 hover:text-slate-200 flex items-center gap-1.5">

          <i class="fa-brands fa-python text-blue-400"></i> executor.py

        </button>

      </div>


      <!-- File Content Container -->

      <div class="flex-1 overflow-auto p-4 bg-slate-950 font-mono text-xs">

        <pre id="skill-file-content" class="text-slate-300 leading-relaxed"></pre>

      </div>


      <!-- Modal Footer -->

      <div class="p-3 border-t border-slate-800 bg-slate-900 flex justify-between items-center text-xs">

        <span class="text-slate-500 text-[11px]"><i class="fa-solid fa-lock mr-1"></i> Read-only file inspection</span>

        <button id="btn-skill-modal-done" class="px-4 py-1.5 bg-slate-800 hover:bg-slate-700 text-slate-200 rounded text-xs transition">Close File View</button>

      </div>

    </div>

  </div>


  <!-- MODAL: EDGE CONDITION EDITOR -->

  <div id="modal-edge-editor" class="fixed inset-0 bg-black/70 backdrop-blur-sm z-50 flex items-center justify-center hidden p-4">

    <div class="bg-slate-900 border border-slate-800 rounded-xl w-full max-w-lg shadow-2xl overflow-hidden">

      <div class="h-12 border-b border-slate-800 px-4 flex items-center justify-between">

        <h3 class="font-bold text-xs uppercase tracking-wider text-slate-200 flex items-center gap-2">

          <i class="fa-solid fa-code-branch text-cyan-400"></i> Directed Edge Condition Rule

        </h3>

        <button id="btn-close-edge-modal" class="text-slate-400 hover:text-white"><i class="fa-solid fa-xmark"></i></button>

      </div>


      <div class="p-4 space-y-4 text-xs">

        <div>

          <label class="text-[10px] uppercase font-mono text-slate-400 block mb-1">Upstream Output Condition (AST Expression)</label>

          <input id="edge-cond-input" type="text" placeholder="e.g. risk_score > 0.7 && compliant == false" class="w-full bg-slate-950 border border-slate-700 rounded p-2 text-xs font-mono text-cyan-300 focus:border-cyan-500 outline-none">

          <p class="text-[11px] text-slate-500 mt-1">Leave empty for unconditional transition. Supports <code class="text-slate-400 font-mono">&gt;, &lt;, ==, !=, &amp;&amp;, ||</code>.</p>

        </div>


        <div class="bg-slate-950 border border-slate-800 p-2.5 rounded">

          <span class="text-[10px] uppercase font-mono text-slate-400 block mb-1">Quick Condition Presets</span>

          <div class="flex flex-wrap gap-1.5">

            <button class="preset-badge px-2 py-1 bg-slate-800 hover:bg-slate-700 rounded text-[11px] font-mono text-slate-300" data-rule="risk_score > 0.7">risk_score &gt; 0.7</button>

            <button class="preset-badge px-2 py-1 bg-slate-800 hover:bg-slate-700 rounded text-[11px] font-mono text-slate-300" data-rule="risk_score <= 0.7">risk_score &lt;= 0.7</button>

            <button class="preset-badge px-2 py-1 bg-slate-800 hover:bg-slate-700 rounded text-[11px] font-mono text-slate-300" data-rule="compliant == true">compliant == true</button>

            <button class="preset-badge px-2 py-1 bg-slate-800 hover:bg-slate-700 rounded text-[11px] font-mono text-slate-300" data-rule="severity == 'Catastrophic'">severity == 'Catastrophic'</button>

          </div>

        </div>


        <div>

          <label class="text-[10px] uppercase font-mono text-slate-400 block mb-1">Available Upstream Output Fields</label>

          <div id="edge-available-fields" class="flex flex-wrap gap-1 p-2 bg-slate-950 border border-slate-800 rounded font-mono text-[10px] text-emerald-400">

            <!-- Rendered dynamically -->

          </div>

        </div>

      </div>


      <div class="p-3 border-t border-slate-800 bg-slate-900/60 flex justify-between items-center">

        <button id="btn-delete-edge" class="text-rose-400 hover:text-rose-300 text-xs px-2 py-1"><i class="fa-solid fa-trash mr-1"></i> Remove Edge</button>

        <div class="flex gap-2">

          <button id="btn-cancel-edge" class="px-3 py-1.5 bg-slate-800 hover:bg-slate-700 text-slate-300 rounded text-xs">Cancel</button>

          <button id="btn-save-edge" class="px-4 py-1.5 bg-cyan-600 hover:bg-cyan-500 text-white font-semibold rounded text-xs shadow-lg shadow-cyan-600/30">Apply Rule</button>

        </div>

      </div>

    </div>

  </div>


  <!-- TOAST NOTIFICATION CONTAINER -->

  <div id="toast" class="fixed bottom-5 right-5 bg-slate-800 border border-slate-700 text-white px-4 py-2.5 rounded-lg shadow-2xl flex items-center gap-2.5 text-xs z-50 transform translate-y-20 opacity-0 transition-all duration-200 pointer-events-none">

    <i id="toast-icon" class="fa-solid fa-check text-emerald-400"></i>

    <span id="toast-msg">Operation completed.</span>

  </div>


  <!-- JAVASCRIPT SIMULATION LOGIC -->

  <script>

    /* ========================================================================

       1. DATA DEFINITIONS: REUSABLE SKILL LIBRARY & AGENT TEMPLATES

       ======================================================================== */

    const SKILL_LIBRARY = {

      hazard_verifier: {

        id: "hazard_verifier",

        name: "Hazard Verifier",

        version: "1.2.0",

        description: "Validates failure mode sequences against NASA-STD-8739.8B requirements.",

        tags: ["safety", "hazards", "compliance"],

        files: {

          "SKILL.md": `---

name: hazard_verifier

version: 1.2.0

description: Validates subsystem hazard causes against design requirements.

tags: [safety, compliance, verification]

---

# Hazard Verifier Skill


Evaluates input failure modes and potential propagation paths against safety criteria.

1. Parse the subsystem identification and critical hazard tags.

2. Determine catastrophic vs moderate failure likelihood.

3. Compute structured compliance boolean and continuous risk_score (0.0 - 1.0).

`,

          "classes.py": `from pydantic import BaseModel, Field

from typing import List, Optional

from enum import Enum


class SeverityLevel(str, Enum):

    CATASTROPHIC = "Catastrophic"

    CRITICAL = "Critical"

    MODERATE = "Moderate"

    NEGLIGIBLE = "Negligible"


class HazardVerifierInput(BaseModel):

    subsystem: str = Field(..., description="Target system module")

    failure_mode: str = Field(..., description="Failure mode symptom")

    safety_standard: str = Field(default="NASA-STD-8739.8B")


class HazardVerifierOutput(BaseModel):

    compliant: bool = Field(..., description="Passes system safety constraints")

    risk_score: float = Field(..., ge=0.0, le=1.0, description="Risk index")

    severity: SeverityLevel = Field(...)

    mitigation_notes: str = Field(..., description="Recommended engineering controls")

`,

          "executor.py": `from typing import Dict, Any

from .classes import HazardVerifierInput, HazardVerifierOutput, SeverityLevel


def execute(raw_inputs: Dict[str, Any]) -> HazardVerifierOutput:

    data = HazardVerifierInput.model_validate(raw_inputs)

    is_avionics = "avionics" in data.subsystem.lower()

    

    # Deterministic simulation logic

    severity = SeverityLevel.CATASTROPHIC if is_avionics else SeverityLevel.MODERATE

    risk = 0.82 if severity == SeverityLevel.CATASTROPHIC else 0.28

    

    return HazardVerifierOutput(

        compliant=(risk <= 0.5),

        risk_score=risk,

        severity=severity,

        mitigation_notes=f"Evaluated against {data.safety_standard}. Redundancy required."

    )

`

        },

        defaultOutput: {

          compliant: false,

          risk_score: 0.82,

          severity: "Catastrophic",

          mitigation_notes: "Single Point Failure detected in Primary Thruster Controller. Escalate to Safety Board."

        }

      },


      system_decomposition: {

        id: "system_decomposition",

        name: "System Decomposition",

        version: "1.0.4",

        description: "Breaks raw engineering reports into discrete subsystem boundaries.",

        tags: ["architecture", "systems-eng"],

        files: {

          "SKILL.md": `---

name: system_decomposition

version: 1.0.4

description: Parses freeform telemetry and incident reports into structured subsystems.

tags: [architecture, parsing]

---

# System Decomposition


Extracts hardware components, telemetry tags, and functional blocks from ingested text.

`,

          "classes.py": `from pydantic import BaseModel, Field

from typing import List


class DecompositionInput(BaseModel):

    raw_report: str = Field(..., description="Unstructured incident or spec log")


class DecompositionOutput(BaseModel):

    subsystem: str = Field(..., description="Parsed subsystem name")

    failure_mode: str = Field(..., description="Extracted symptom")

    components_involved: List[str] = Field(default_factory=list)

    confidence: float = Field(..., ge=0.0, le=1.0)

`,

          "executor.py": `from typing import Dict, Any

from .classes import DecompositionInput, DecompositionOutput


def execute(raw_inputs: Dict[str, Any]) -> DecompositionOutput:

    inp = DecompositionInput.model_validate(raw_inputs)

    return DecompositionOutput(

        subsystem="Reaction Control Avionics (RCS-1)",

        failure_mode="Solenoid Valve Stuck Open",

        components_involved=["Valve-A4", "Driver-FET-12"],

        confidence=0.96

    )

`

        },

        defaultOutput: {

          subsystem: "Reaction Control Avionics (RCS-1)",

          failure_mode: "Solenoid Valve Stuck Open",

          components_involved: ["Valve-A4", "Driver-FET-12"],

          confidence: 0.96

        }

      },


      code_linter: {

        id: "code_linter",

        name: "Code Linter & Static QA",

        version: "2.1.0",

        description: "Enforces MISRA-C and PEP8 strict architectural standards.",

        tags: ["qa", "linter", "static-analysis"],

        files: {

          "SKILL.md": `# Code Linter Skill\nVerifies source code syntax and standards.`,

          "classes.py": `from pydantic import BaseModel, Field

from typing import List


class LintInput(BaseModel):

    code_snippet: str


class LintOutput(BaseModel):

    passed: bool

    violations: List[str]

    cyclomatic_complexity: int

`,

          "executor.py": `def execute(inputs):

    return {"passed": True, "violations": [], "cyclomatic_complexity": 3}`

        },

        defaultOutput: {

          passed: true,

          violations: [],

          cyclomatic_complexity: 4

        }

      },


      sentiment_evaluator: {

        id: "sentiment_evaluator",

        name: "Sentiment & Priority Classifier",

        version: "1.1.2",

        description: "Evaluates incident urgency and urgency polarity from user communications.",

        tags: ["nlp", "triage"],

        files: {

          "SKILL.md": `# Sentiment Classifier\nDetermines priority levels from communications.`,

          "classes.py": `from pydantic import BaseModel\nclass TriageOutput(BaseModel):\n    urgency: str\n    priority_rank: int`,

          "executor.py": `def execute(inp): return {"urgency": "HIGH", "priority_rank": 1}`

        },

        defaultOutput: {

          urgency: "HIGH",

          priority_rank: 1

        }

      }

    };


    const AGENT_TEMPLATES = [

      {

        templateId: "systems_engineer",

        name: "Systems Engineer",

        model: "gemini-2.5-pro",

        icon: "fa-compass-drafting",

        color: "from-blue-600 to-cyan-700",

        badgeColor: "bg-blue-900/60 text-blue-300 border-blue-700",

        skills: ["system_decomposition"],

        description: "Parses incident logs and breaks down engineering architectures."

      },

      {

        templateId: "safety_analyst",

        name: "Safety & Hazard Analyst",

        model: "gemini-2.5-pro",

        icon: "fa-shield-halved",

        color: "from-amber-600 to-orange-700",

        badgeColor: "bg-amber-900/60 text-amber-300 border-amber-700",

        skills: ["hazard_verifier"],

        description: "Performs strict hazard screening against NASA guidelines."

      },

      {

        templateId: "qa_gatekeeper",

        name: "QA Gatekeeper",

        model: "gemini-2.5-flash",

        icon: "fa-clipboard-check",

        color: "from-emerald-600 to-teal-700",

        badgeColor: "bg-emerald-900/60 text-emerald-300 border-emerald-700",

        skills: ["code_linter"],

        description: "Final approval gate for design and verification integrity."

      },

      {

        templateId: "escalation_mgr",

        name: "Escalation Manager",

        model: "gemini-2.5-flash",

        icon: "fa-triangle-exclamation",

        color: "from-rose-600 to-red-700",

        badgeColor: "bg-rose-900/60 text-rose-300 border-rose-700",

        skills: ["sentiment_evaluator"],

        description: "Handles catastrophic failure pathways and critical incident alerts."

      }

    ];


    /* ========================================================================

       2. WORKFLOW CANVAS APPLICATION STATE

       ======================================================================== */

    const state = {

      nodes: [],

      edges: [],

      selectedNodeId: null,

      selectedEdgeId: null,

      connectingPort: null, // { nodeId, portType: 'out' }

      zoom: 1.0,

      executionSpeed: 1000, // ms per step

      isExecuting: false,

      context: {

        workflow: {

          input: {

            report_text: "Telemetry spike on RCS-1 thruster quad during burn sequence.",

            author: "Mission Control Subsystem Lead"

          }

        }

      },

      logs: []

    };


    /* ========================================================================

       3. DOM REFERENCES

       ======================================================================== */

    const canvasContainer = document.getElementById("canvas-container");

    const nodesContainer = document.getElementById("nodes-container");

    const svgLayer = document.getElementById("svg-layer");

    const edgesGroup = document.getElementById("edges-group");

    const tempEdge = document.getElementById("temp-edge");


    // UI Panels

    const agentPaletteList = document.getElementById("agent-palette-list");

    const skillLibraryList = document.getElementById("skill-library-list");

    const contextViewer = document.getElementById("context-viewer");

    const traceLogs = document.getElementById("trace-logs");

    const logCountBadge = document.getElementById("log-count");

    const astPreview = document.getElementById("ast-preview");

    const astEvalBadge = document.getElementById("ast-eval-badge");


    // Modals

    const modalSkillViewer = document.getElementById("modal-skill-viewer");

    const modalEdgeEditor = document.getElementById("modal-edge-editor");


    // Toasts

    const toast = document.getElementById("toast");

    const toastMsg = document.getElementById("toast-msg");

    const toastIcon = document.getElementById("toast-icon");


    /* ========================================================================

       4. INITIALIZATION & TEMPLATE RENDERING

       ======================================================================== */

    function init() {

      renderAgentPalette();

      renderSkillLibrary();

      setupTabs();

      setupCanvasEventListeners();

      setupHeaderControls();

      loadSampleWorkflow();

      updateContextUI();

    }


    function showToast(msg, isSuccess = true) {

      toastMsg.innerText = msg;

      toastIcon.className = isSuccess ? "fa-solid fa-check text-emerald-400" : "fa-solid fa-triangle-exclamation text-amber-400";

      toast.classList.remove("translate-y-20", "opacity-0");

      setTimeout(() => {

        toast.classList.add("translate-y-20", "opacity-0");

      }, 2500);

    }


    function addLog(msg, level = "info") {

      const time = new Date().toLocaleTimeString();

      let color = "text-slate-300";

      if (level === "success") color = "text-emerald-400";

      if (level === "warn") color = "text-amber-400";

      if (level === "error") color = "text-rose-400";

      if (level === "edge") color = "text-cyan-400";


      state.logs.unshift({ time, msg, color });

      logCountBadge.innerText = `${state.logs.length} events`;

      

      traceLogs.innerHTML = state.logs

        .slice(0, 50)

        .map(l => `<div class="${l.color}"><span class="text-slate-600">[${l.time}]</span> ${l.msg}</div>`)

        .join("");

    }


    function updateContextUI() {

      contextViewer.innerText = JSON.stringify(state.context, null, 2);

    }


    /* Render Draggable Agents in Left Palette */

    function renderAgentPalette() {

      agentPaletteList.innerHTML = AGENT_TEMPLATES.map(tpl => `

        <div draggable="true" 

             data-template-id="${tpl.templateId}"

             class="agent-drag-item p-3 rounded-lg bg-slate-950/80 border border-slate-800 hover:border-cyan-500/60 cursor-grab active:cursor-grabbing transition shadow-sm hover:shadow-cyan-900/10">

          <div class="flex items-center justify-between mb-1.5">

            <div class="flex items-center gap-2">

              <div class="w-6 h-6 rounded bg-gradient-to-tr ${tpl.color} flex items-center justify-center text-[10px] text-white">

                <i class="fa-solid ${tpl.icon}"></i>

              </div>

              <span class="text-xs font-semibold text-slate-200">${tpl.name}</span>

            </div>

            <span class="text-[9px] font-mono px-1.5 py-0.5 rounded ${tpl.badgeColor} border">${tpl.model.split('-').slice(1).join(' ')}</span>

          </div>

          <p class="text-[11px] text-slate-400 mb-2 leading-tight">${tpl.description}</p>

          <div class="flex flex-wrap gap-1">

            ${tpl.skills.map(s => `

              <span class="text-[9px] font-mono px-1.5 py-0.2 rounded bg-slate-800/90 text-cyan-400 border border-slate-700 flex items-center gap-1">

                <i class="fa-solid fa-cube text-[8px]"></i> ${s}

              </span>

            `).join('')}

          </div>

        </div>

      `).join('');


      document.querySelectorAll(".agent-drag-item").forEach(item => {

        item.addEventListener("dragstart", (e) => {

          e.dataTransfer.setData("application/agent-template", item.dataset.templateId);

        });

      });

    }


    /* Render Reusable Skill Library Files in Left Sidebar */

    function renderSkillLibrary() {

      skillLibraryList.innerHTML = Object.values(SKILL_LIBRARY).map(skill => `

        <div class="p-2.5 rounded-lg bg-slate-950 border border-slate-800 hover:border-slate-700 transition">

          <div class="flex items-center justify-between mb-1">

            <div class="flex items-center gap-2">

              <i class="fa-solid fa-folder-closed text-amber-400 text-xs"></i>

              <span class="text-xs font-semibold text-slate-200">${skill.id}</span>

            </div>

            <span class="text-[9px] font-mono text-slate-500">v${skill.version}</span>

          </div>

          <p class="text-[11px] text-slate-400 mb-2">${skill.description}</p>

          

          <div class="flex items-center justify-between border-t border-slate-800/80 pt-2">

            <div class="flex gap-1 text-[10px] text-slate-400 font-mono">

              <span>.md</span> • <span>.json</span> • <span>.py</span>

            </div>

            <button onclick="openSkillViewer('${skill.id}')" class="px-2 py-0.5 rounded bg-slate-800 hover:bg-slate-700 text-[10px] text-cyan-400 border border-slate-700 transition flex items-center gap-1">

              <i class="fa-regular fa-eye"></i> Inspect

            </button>

          </div>

        </div>

      `).join('');

    }


    /* Left and Right Tabs Logic */

    function setupTabs() {

      const tabAgents = document.getElementById("tab-agents");

      const tabSkills = document.getElementById("tab-skills");

      const tabBtnAgents = document.getElementById("tab-btn-agents");

      const tabBtnSkills = document.getElementById("tab-btn-skills");


      tabBtnAgents.addEventListener("click", () => {

        tabAgents.classList.remove("hidden");

        tabSkills.classList.add("hidden");

        tabBtnAgents.className = "flex-1 py-2.5 text-xs font-medium border-b-2 border-cyan-500 text-cyan-400 flex items-center justify-center gap-2";

        tabBtnSkills.className = "flex-1 py-2.5 text-xs font-medium border-b-2 border-transparent text-slate-400 hover:text-slate-200 flex items-center justify-center gap-2";

      });


      tabBtnSkills.addEventListener("click", () => {

        tabSkills.classList.remove("hidden");

        tabAgents.classList.add("hidden");

        tabBtnSkills.className = "flex-1 py-2.5 text-xs font-medium border-b-2 border-cyan-500 text-cyan-400 flex items-center justify-center gap-2";

        tabBtnAgents.className = "flex-1 py-2.5 text-xs font-medium border-b-2 border-transparent text-slate-400 hover:text-slate-200 flex items-center justify-center gap-2";

      });


      const tabTrace = document.getElementById("view-trace");

      const tabNodeCfg = document.getElementById("view-nodecfg");

      const tabBtnTrace = document.getElementById("tab-btn-trace");

      const tabBtnNodeCfg = document.getElementById("tab-btn-nodecfg");


      tabBtnTrace.addEventListener("click", () => {

        tabTrace.classList.remove("hidden");

        tabNodeCfg.classList.add("hidden");

        tabBtnTrace.className = "flex-1 py-1.5 font-medium border-b-2 border-cyan-500 text-cyan-400";

        tabBtnNodeCfg.className = "flex-1 py-1.5 font-medium border-b-2 border-transparent text-slate-400 hover:text-slate-200";

      });


      tabBtnNodeCfg.addEventListener("click", () => {

        tabNodeCfg.classList.remove("hidden");

        tabTrace.classList.add("hidden");

        tabBtnNodeCfg.className = "flex-1 py-1.5 font-medium border-b-2 border-cyan-500 text-cyan-400";

        tabBtnTrace.className = "flex-1 py-1.5 font-medium border-b-2 border-transparent text-slate-400 hover:text-slate-200";

      });


      document.getElementById("btn-clear-trace").addEventListener("click", () => {

        state.logs = [];

        traceLogs.innerHTML = `<div class="text-slate-500 italic">Logs cleared.</div>`;

        logCountBadge.innerText = "0 events";

      });

    }


    /* ========================================================================

       5. WORKFLOW CANVAS LOGIC (NODES & DIRECTED EDGES)

       ======================================================================== */

    function setupCanvasEventListeners() {

      // Drag over canvas to allow dropping nodes

      canvasContainer.addEventListener("dragover", (e) => {

        e.preventDefault();

        e.dataTransfer.dropEffect = "copy";

      });


      canvasContainer.addEventListener("drop", (e) => {

        e.preventDefault();

        const templateId = e.dataTransfer.getData("application/agent-template");

        if (!templateId) return;


        const rect = canvasContainer.getBoundingClientRect();

        const x = e.clientX - rect.left - 110;

        const y = e.clientY - rect.top - 40;


        createNodeFromTemplate(templateId, Math.max(20, x), Math.max(20, y));

      });


      // Canvas background click clears selection

      canvasContainer.addEventListener("click", (e) => {

        if (e.target === canvasContainer || e.target === svgLayer) {

          clearSelections();

        }

      });


      // Connection tracking with mouse move

      window.addEventListener("mousemove", (e) => {

        if (!state.connectingPort) return;

        const rect = canvasContainer.getBoundingClientRect();

        const sourceNode = state.nodes.find(n => n.id === state.connectingPort.nodeId);

        if (!sourceNode) return;


        const startX = sourceNode.x + 220; // right edge port

        const startY = sourceNode.y + 60;

        const endX = e.clientX - rect.left;

        const endY = e.clientY - rect.top;


        tempEdge.setAttribute("d", createCurvedPath(startX, startY, endX, endY));

        tempEdge.classList.remove("hidden");

      });


      window.addEventListener("mouseup", () => {

        if (state.connectingPort) {

          state.connectingPort = null;

          tempEdge.classList.add("hidden");

        }

      });

    }


    function createCurvedPath(x1, y1, x2, y2) {

      const dx = Math.abs(x2 - x1) * 0.55;

      return `M ${x1} ${y1} C ${x1 + dx} ${y1}, ${x2 - dx} ${y2}, ${x2} ${y2}`;

    }


    function clearSelections() {

      state.selectedNodeId = null;

      state.selectedEdgeId = null;

      document.querySelectorAll(".agent-node").forEach(n => n.classList.remove("ring-2", "ring-cyan-500"));

      document.querySelectorAll(".workflow-edge").forEach(e => e.classList.remove("stroke-cyan-400"));

      

      document.getElementById("no-node-selected").classList.remove("hidden");

      document.getElementById("node-config-form").classList.add("hidden");

    }


    function createNodeFromTemplate(templateId, x, y, customId = null, overrideOutput = null) {

      const tpl = AGENT_TEMPLATES.find(t => t.templateId === templateId);

      if (!tpl) return;


      const nodeId = customId || `node_${templateId}_${Date.now().toString().slice(-4)}`;

      

      // Determine primary skill output template

      let outputData = overrideOutput;

      if (!outputData && tpl.skills.length > 0) {

        const skill = SKILL_LIBRARY[tpl.skills[0]];

        outputData = skill ? JSON.parse(JSON.stringify(skill.defaultOutput)) : { status: "executed" };

      }


      const nodeObj = {

        id: nodeId,

        templateId: tpl.templateId,

        name: tpl.name,

        model: tpl.model,

        icon: tpl.icon,

        color: tpl.color,

        skills: [...tpl.skills],

        x: x,

        y: y,

        status: "idle", // idle, running, passed, skipped, branched

        output: outputData

      };


      state.nodes.push(nodeObj);

      renderNodes();

      addLog(`Added agent node [${nodeObj.name}] (${nodeId})`);

      return nodeObj;

    }


    function renderNodes() {

      nodesContainer.innerHTML = "";


      state.nodes.forEach(node => {

        const el = document.createElement("div");

        el.id = `el-${node.id}`;

        el.className = `agent-node absolute w-56 rounded-xl bg-slate-900/95 border border-slate-800 shadow-xl pointer-events-auto transition-shadow cursor-move ${getNodeStatusClass(node.status)}`;

        el.style.left = `${node.x}px`;

        el.style.top = `${node.y}px`;


        if (state.selectedNodeId === node.id) {

          el.classList.add("ring-2", "ring-cyan-500");

        }


        el.innerHTML = `

          <!-- Header -->

          <div class="p-2.5 border-b border-slate-800/80 flex items-center justify-between">

            <div class="flex items-center gap-2 truncate">

              <div class="w-6 h-6 rounded bg-gradient-to-tr ${node.color} flex items-center justify-center text-[10px] text-white flex-shrink-0">

                <i class="fa-solid ${node.icon}"></i>

              </div>

              <span class="text-xs font-bold text-slate-200 truncate">${node.name}</span>

            </div>

            <button class="btn-del-node text-slate-500 hover:text-rose-400 p-1 text-[11px]"><i class="fa-solid fa-xmark"></i></button>

          </div>


          <!-- Body -->

          <div class="p-2.5 space-y-2">

            <div class="flex items-center justify-between text-[10px]">

              <span class="font-mono text-slate-400">${node.id}</span>

              <span class="font-mono text-[9px] px-1.5 py-0.2 rounded bg-slate-800 text-cyan-300 border border-slate-700">${node.model.split('-').slice(1).join(' ')}</span>

            </div>


            <div class="flex flex-wrap gap-1">

              ${node.skills.map(s => `

                <span class="text-[9px] font-mono px-1.5 py-0.5 rounded bg-slate-950 text-slate-300 border border-slate-800 flex items-center gap-1">

                  <i class="fa-solid fa-code text-[8px] text-cyan-400"></i> ${s}

                </span>

              `).join('')}

            </div>


            <!-- Execution status badge -->

            <div class="pt-1 border-t border-slate-800/60 flex items-center justify-between text-[10px] font-mono">

              <span class="text-slate-500">Status</span>

              <span class="${getStatusBadgeStyle(node.status)}">${node.status.toUpperCase()}</span>

            </div>

          </div>


          <!-- Input Port (Left) -->

          <div data-port="in" data-node="${node.id}" title="Input Port" class="handle-port absolute -left-2.5 top-1/2 -translate-y-1/2 w-5 h-5 rounded-full bg-slate-800 border-2 border-indigo-400 flex items-center justify-center cursor-pointer transition z-30">

            <div class="w-1.5 h-1.5 rounded-full bg-indigo-300"></div>

          </div>


          <!-- Output Port (Right) -->

          <div data-port="out" data-node="${node.id}" title="Output Port (Drag to connect)" class="handle-port absolute -right-2.5 top-1/2 -translate-y-1/2 w-5 h-5 rounded-full bg-slate-800 border-2 border-cyan-400 flex items-center justify-center cursor-pointer transition z-30">

            <div class="w-1.5 h-1.5 rounded-full bg-cyan-300"></div>

          </div>

        `;


        // Node Dragging Logic

        makeNodeDraggable(el, node);


        // Selection

        el.addEventListener("click", (e) => {

          if (e.target.closest(".handle-port") || e.target.closest(".btn-del-node")) return;

          selectNode(node.id);

        });


        // Delete button

        el.querySelector(".btn-del-node").addEventListener("click", (e) => {

          e.stopPropagation();

          deleteNode(node.id);

        });


        // Port Connection Logic

        const outPort = el.querySelector('[data-port="out"]');

        outPort.addEventListener("mousedown", (e) => {

          e.stopPropagation();

          state.connectingPort = { nodeId: node.id, portType: 'out' };

        });


        const inPort = el.querySelector('[data-port="in"]');

        inPort.addEventListener("mouseup", (e) => {

          e.stopPropagation();

          if (state.connectingPort && state.connectingPort.nodeId !== node.id) {

            createEdge(state.connectingPort.nodeId, node.id);

            state.connectingPort = null;

            tempEdge.classList.add("hidden");

          }

        });


        nodesContainer.appendChild(el);

      });


      renderEdges();

    }


    function getNodeStatusClass(status) {

      switch (status) {

        case "running": return "node-running";

        case "passed": return "node-passed";

        case "branched": return "node-branched";

        case "skipped": return "node-skipped";

        default: return "";

      }

    }


    function getStatusBadgeStyle(status) {

      switch (status) {

        case "running": return "text-cyan-400 animate-pulse font-bold";

        case "passed": return "text-emerald-400 font-bold";

        case "branched": return "text-amber-400 font-bold";

        case "skipped": return "text-slate-500 line-through";

        default: return "text-slate-400";

      }

    }


    function makeNodeDraggable(element, nodeObj) {

      let isDragging = false;

      let startX, startY;


      element.addEventListener("mousedown", (e) => {

        if (e.target.closest(".handle-port") || e.target.closest(".btn-del-node") || e.target.closest("button")) return;

        isDragging = true;

        startX = e.clientX - nodeObj.x;

        startY = e.clientY - nodeObj.y;

        element.style.zIndex = "40";


        function onMouseMove(moveEvent) {

          if (!isDragging) return;

          nodeObj.x = Math.max(10, moveEvent.clientX - startX);

          nodeObj.y = Math.max(10, moveEvent.clientY - startY);

          element.style.left = `${nodeObj.x}px`;

          element.style.top = `${nodeObj.y}px`;

          renderEdges();

        }


        function onMouseUp() {

          isDragging = false;

          element.style.zIndex = "10";

          window.removeEventListener("mousemove", onMouseMove);

          window.removeEventListener("mouseup", onMouseUp);

        }


        window.addEventListener("mousemove", onMouseMove);

        window.addEventListener("mouseup", onMouseUp);

      });

    }


    /* Edge Connections */

    function createEdge(sourceId, targetId, condition = null) {

      // Avoid duplicate edge in same direction

      const exists = state.edges.some(e => e.source === sourceId && e.target === targetId);

      if (exists) {

        showToast("Connection already exists between nodes.", false);

        return;

      }


      const edgeId = `edge_${sourceId}_to_${targetId}`;

      state.edges.push({

        id: edgeId,

        source: sourceId,

        target: targetId,

        condition: condition, // e.g. "risk_score > 0.7"

        status: "idle" // idle, active, true, false

      });


      addLog(`Connected [${sourceId}] ➔ [${targetId}] ${condition ? `with condition: (${condition})` : '(unconditional)'}`);

      renderEdges();

      showToast("Directed edge connected.");

    }


    function renderEdges() {

      edgesGroup.innerHTML = "";


      state.edges.forEach(edge => {

        const sourceNode = state.nodes.find(n => n.id === edge.source);

        const targetNode = state.nodes.find(n => n.id === edge.target);

        if (!sourceNode || !targetNode) return;


        const x1 = sourceNode.x + 224; // output port X

        const y1 = sourceNode.y + 60;

        const x2 = targetNode.x;      // input port X

        const y2 = targetNode.y + 60;


        const pathD = createCurvedPath(x1, y1, x2, y2);


        // Path Element

        const path = document.createElementNS("http://www.w3.org/2000/svg", "path");

        path.setAttribute("d", pathD);

        path.setAttribute("id", edge.id);

        

        let marker = "url(#arrow)";

        let strokeClass = "stroke-slate-600 stroke-[2px]";


        if (edge.status === "active") {

          strokeClass = "edge-active";

          marker = "url(#arrow-active)";

        } else if (edge.status === "true") {

          strokeClass = "edge-true";

          marker = "url(#arrow-true)";

        } else if (edge.status === "false") {

          strokeClass = "edge-false";

          marker = "url(#arrow-false)";

        }


        path.setAttribute("class", `workflow-edge fill-none cursor-pointer hover:stroke-cyan-400 transition-colors ${strokeClass}`);

        path.setAttribute("marker-end", marker);


        // Click to open Condition modal

        path.addEventListener("click", (e) => {

          e.stopPropagation();

          openEdgeEditor(edge.id);

        });


        edgesGroup.appendChild(path);


        // Midpoint condition label

        const midX = (x1 + x2) / 2;

        const midY = (y1 + y2) / 2;


        const g = document.createElementNS("http://www.w3.org/2000/svg", "g");

        g.setAttribute("class", "cursor-pointer");

        g.addEventListener("click", (e) => {

          e.stopPropagation();

          openEdgeEditor(edge.id);

        });


        const displayText = edge.condition ? edge.condition : "always";

        const isConditional = !!edge.condition;


        g.innerHTML = `

          <rect x="${midX - 35}" y="${midY - 11}" width="70" height="22" rx="4" 

                class="${isConditional ? 'fill-slate-900 stroke-cyan-500/60' : 'fill-slate-900/90 stroke-slate-700'} stroke-[1px]"/>

          <text x="${midX}" y="${midY + 3.5}" text-anchor="middle" font-size="9.5" font-family="monospace" 

                class="${isConditional ? 'fill-cyan-300 font-bold' : 'fill-slate-400'}">

            ${truncateText(displayText, 10)}

          </text>

        `;

        edgesGroup.appendChild(g);

      });

    }


    function truncateText(str, n) {

      return (str.length > n) ? str.substr(0, n - 1) + '…' : str;

    }


    function deleteNode(nodeId) {

      state.nodes = state.nodes.filter(n => n.id !== nodeId);

      state.edges = state.edges.filter(e => e.source !== nodeId && e.target !== nodeId);

      if (state.selectedNodeId === nodeId) clearSelections();

      renderNodes();

      addLog(`Removed node [${nodeId}]`, "warn");

    }


    function selectNode(nodeId) {

      state.selectedNodeId = nodeId;

      renderNodes();


      const node = state.nodes.find(n => n.id === nodeId);

      if (!node) return;


      // Switch to node config tab

      document.getElementById("tab-btn-nodecfg").click();

      document.getElementById("no-node-selected").classList.add("hidden");

      document.getElementById("node-config-form").classList.remove("hidden");


      document.getElementById("cfg-node-id").value = node.id;

      document.getElementById("cfg-agent-name").value = node.name;

      document.getElementById("cfg-agent-model").value = node.model;

      document.getElementById("cfg-mock-output").value = JSON.stringify(node.output || {}, null, 2);


      const assignedContainer = document.getElementById("cfg-assigned-skills");

      assignedContainer.innerHTML = node.skills.map(s => `

        <span class="text-[10px] font-mono px-2 py-0.5 rounded bg-slate-900 text-cyan-400 border border-slate-700 flex items-center gap-1">

          <i class="fa-solid fa-cube text-[8px]"></i> ${s}

        </span>

      `).join('');

    }


    // Save Node Config edits

    document.getElementById("btn-save-nodecfg").addEventListener("click", () => {

      if (!state.selectedNodeId) return;

      const node = state.nodes.find(n => n.id === state.selectedNodeId);

      if (!node) return;


      try {

        node.name = document.getElementById("cfg-agent-name").value;

        node.model = document.getElementById("cfg-agent-model").value;

        node.output = JSON.parse(document.getElementById("cfg-mock-output").value);

        renderNodes();

        showToast("Node settings updated.");

        addLog(`Updated properties for node [${node.id}]`);

      } catch (err) {

        showToast("Invalid JSON in simulated output override.", false);

      }

    });


    /* ========================================================================

       6. EDGE CONDITION MODAL & AST EVALUATOR

       ======================================================================== */

    function openEdgeEditor(edgeId) {

      const edge = state.edges.find(e => e.id === edgeId);

      if (!edge) return;

      state.selectedEdgeId = edgeId;


      const sourceNode = state.nodes.find(n => n.id === edge.source);

      const edgeCondInput = document.getElementById("edge-cond-input");

      edgeCondInput.value = edge.condition || "";


      // List available fields from source node output

      const fieldsContainer = document.getElementById("edge-available-fields");

      if (sourceNode && sourceNode.output) {

        fieldsContainer.innerHTML = Object.keys(sourceNode.output).map(k => `

          <button type="button" class="field-pill px-1.5 py-0.5 rounded bg-slate-900 border border-slate-700 hover:border-cyan-500 text-emerald-400" data-field="${k}">

            ${k} (${typeof sourceNode.output[k]})

          </button>

        `).join('');


        fieldsContainer.querySelectorAll(".field-pill").forEach(pill => {

          pill.addEventListener("click", () => {

            edgeCondInput.value += ` ${pill.dataset.field} `;

            edgeCondInput.focus();

          });

        });

      } else {

        fieldsContainer.innerHTML = `<span class="text-slate-500 italic">No output schema defined for ${edge.source}</span>`;

      }


      modalEdgeEditor.classList.remove("hidden");

    }


    document.querySelectorAll(".preset-badge").forEach(badge => {

      badge.addEventListener("click", () => {

        document.getElementById("edge-cond-input").value = badge.dataset.rule;

      });

    });


    document.getElementById("btn-close-edge-modal").addEventListener("click", () => modalEdgeEditor.classList.add("hidden"));

    document.getElementById("btn-cancel-edge").addEventListener("click", () => modalEdgeEditor.classList.add("hidden"));


    document.getElementById("btn-save-edge").addEventListener("click", () => {

      if (!state.selectedEdgeId) return;

      const edge = state.edges.find(e => e.id === state.selectedEdgeId);

      if (edge) {

        const val = document.getElementById("edge-cond-input").value.trim();

        edge.condition = val.length > 0 ? val : null;

        renderEdges();

        modalEdgeEditor.classList.add("hidden");

        showToast("Edge transition rule updated.");

        addLog(`Edge [${edge.id}] updated condition ➔ ${edge.condition || 'always'}`);

      }

    });


    document.getElementById("btn-delete-edge").addEventListener("click", () => {

      if (!state.selectedEdgeId) return;

      state.edges = state.edges.filter(e => e.id !== state.selectedEdgeId);

      renderEdges();

      modalEdgeEditor.classList.add("hidden");

      showToast("Edge removed.", false);

      addLog(`Deleted edge [${state.selectedEdgeId}]`, "warn");

    });


    /* Safe AST Boolean Evaluator */

    function evaluateAstCondition(expr, scope) {

      if (!expr || expr.trim() === "") return true;


      try {

        // Substitute known scope keys into expression

        let evalString = expr;


        // Replace boolean literals

        evalString = evalString.replace(/\btrue\b/gi, "true").replace(/\bfalse\b/gi, "false");


        // Safely extract token identifiers

        const tokens = expr.match(/[a-zA-Z_][a-zA-Z0-9_]*/g) || [];

        for (const token of tokens) {

          if (["true", "false", "null", "undefined"].includes(token)) continue;

          if (token in scope) {

            const val = scope[token];

            const safeVal = typeof val === "string" ? JSON.stringify(val) : val;

            evalString = evalString.replaceAll(new RegExp(`\\b${token}\\b`, 'g'), safeVal);

          }

        }


        // Evaluate using safe Function constructor restricted to boolean return

        const result = Boolean(new Function(`"use strict"; return (${evalString});`)());

        return { success: true, result, evaluatedExpr: evalString };

      } catch (err) {

        return { success: false, result: false, error: err.message };

      }

    }


    /* ========================================================================

       7. STEP-BY-STEP SIMULATION ENGINE (DAG TRAVERSAL)

       ======================================================================== */

    async function runSimulation() {

      if (state.isExecuting) return;

      if (state.nodes.length === 0) {

        showToast("Canvas is empty. Add nodes to execute.", false);

        return;

      }


      state.isExecuting = true;

      document.getElementById("btn-run").disabled = true;

      document.getElementById("btn-run").innerHTML = `<i class="fa-solid fa-spinner fa-spin"></i> Running...`;


      // Reset statuses

      state.nodes.forEach(n => n.status = "idle");

      state.edges.forEach(e => e.status = "idle");

      renderNodes();


      addLog("Starting DAG workflow execution...", "info");

      document.getElementById("tab-btn-trace").click();


      // Find root nodes (in-degree == 0)

      const targetNodeIds = new Set(state.edges.map(e => e.target));

      let currentQueue = state.nodes.filter(n => !targetNodeIds.has(n.id)).map(n => n.id);


      if (currentQueue.length === 0 && state.nodes.length > 0) {

        // Fallback to first node if cyclic

        currentQueue = [state.nodes[0].id];

      }


      const executedNodes = new Set();


      while (currentQueue.length > 0) {

        const currId = currentQueue.shift();

        const currNode = state.nodes.find(n => n.id === currId);

        if (!currNode || executedNodes.has(currId)) continue;


        // Mark Running

        currNode.status = "running";

        renderNodes();

        addLog(`▶ [${currNode.name}] executing with model ${currNode.model}...`);


        await sleep(state.executionSpeed);


        // Store output into global execution context

        state.context[currId] = {

          agent: currNode.name,

          model: currNode.model,

          output: currNode.output || { executed: true }

        };

        updateContextUI();


        currNode.status = "passed";

        executedNodes.add(currId);

        renderNodes();

        addLog(`✔ [${currNode.name}] finished. Output committed to context.`, "success");


        // Inspect outgoing edges

        const outgoingEdges = state.edges.filter(e => e.source === currId);

        let branchCount = 0;


        for (const edge of outgoingEdges) {

          edge.status = "active";

          renderEdges();

          await sleep(state.executionSpeed * 0.4);


          // Evaluate condition against current node's output

          const scope = currNode.output || {};

          const evalRes = evaluateAstCondition(edge.condition, scope);


          if (evalRes.result) {

            edge.status = "true";

            branchCount++;

            currentQueue.push(edge.target);

            

            astPreview.innerHTML = `

              <span class="text-emerald-400 font-bold">TRUE:</span> 

              <span class="text-slate-300 ml-1">(${edge.condition || 'always'})</span>

              <span class="text-slate-500 ml-2">➔ ${edge.target}</span>

            `;

            astEvalBadge.innerText = "PASSED";

            astEvalBadge.className = "text-[9px] px-1.5 py-0.5 rounded bg-emerald-950 text-emerald-300 font-mono";


            addLog(`Branch [${edge.source} ➔ ${edge.target}] matched condition (${edge.condition || 'default'}). Traversing.`, "edge");

          } else {

            edge.status = "false";

            // Mark target skipped if not reachable

            const targetNode = state.nodes.find(n => n.id === edge.target);

            if (targetNode && !currentQueue.includes(edge.target)) {

              targetNode.status = "skipped";

            }


            astPreview.innerHTML = `

              <span class="text-rose-400 font-bold">FALSE:</span> 

              <span class="text-slate-400 ml-1">(${edge.condition})</span>

              <span class="text-slate-500 ml-2">✖ Branch pruned</span>

            `;

            astEvalBadge.innerText = "PRUNED";

            astEvalBadge.className = "text-[9px] px-1.5 py-0.5 rounded bg-rose-950 text-rose-300 font-mono";


            addLog(`Branch [${edge.source} ➔ ${edge.target}] condition (${edge.condition}) evaluated to FALSE. Skipped.`, "warn");

          }


          renderEdges();

          renderNodes();

          await sleep(state.executionSpeed * 0.3);

        }


        if (branchCount > 1) {

          currNode.status = "branched";

          renderNodes();

        }

      }


      state.isExecuting = false;

      document.getElementById("btn-run").disabled = false;

      document.getElementById("btn-run").innerHTML = `<i class="fa-solid fa-play"></i> Run Simulation`;

      showToast("Simulation completed successfully!");

      addLog("Workflow DAG execution completed.", "success");

    }


    function sleep(ms) {

      return new Promise(resolve => setTimeout(resolve, ms));

    }


    /* Contract Validation Logic */

    function validatePipeline() {

      if (state.edges.length === 0) {

        showToast("Add edges between nodes to validate contracts.", false);

        return;

      }


      let validCount = 0;

      let warnings = 0;


      state.edges.forEach(edge => {

        const source = state.nodes.find(n => n.id === edge.source);

        const target = state.nodes.find(n => n.id === edge.target);


        if (!source || !target) return;


        // Check if target requires inputs that source doesn't provide

        if (target.templateId === "safety_analyst" && source.templateId === "systems_engineer") {

          validCount++;

        } else if (edge.condition) {

          const evalTest = evaluateAstCondition(edge.condition, source.output || {});

          if (!evalTest.success) {

            addLog(`Validation warning on edge [${edge.id}]: Condition syntax invalid (${evalTest.error})`, "warn");

            warnings++;

          } else {

            validCount++;

          }

        } else {

          validCount++;

        }

      });


      if (warnings === 0) {

        showToast(`All ${validCount} directed contracts and Pydantic schemas validated!`);

        addLog(`Verified ${validCount} node contracts. System is fully compatible.`, "success");

      } else {

        showToast(`Validation finished with ${warnings} syntax warnings. Check logs.`, false);

      }

    }


    /* Sample Flow Loader */

    function loadSampleWorkflow() {

      state.nodes = [];

      state.edges = [];

      clearSelections();


      // Node 1: Ingestion / Systems Engineer

      const n1 = createNodeFromTemplate("systems_engineer", 60, 160, "node_ingest");

      

      // Node 2: Safety Analyst

      const n2 = createNodeFromTemplate("safety_analyst", 360, 160, "node_safety");


      // Node 3: Escalation Manager (Triggered if risk_score > 0.7)

      const n3 = createNodeFromTemplate("escalation_mgr", 680, 60, "node_escalation", {

        alert_sent: true,

        incident_commander: "On-Duty Director",

        triage_status: "LEVEL-1 CRITICAL"

      });


      // Node 4: QA Gatekeeper / Approval (Triggered if risk_score <= 0.7)

      const n4 = createNodeFromTemplate("qa_gatekeeper", 680, 270, "node_qa_gate", {

        qa_approved: true,

        release_stamp: "VERIFIED-MISRA-2026",

        audit_log_id: "QA-9021"

      });


      // Edges with conditional branching

      createEdge(n1.id, n2.id, null); // unconditional

      createEdge(n2.id, n3.id, "risk_score > 0.7");

      createEdge(n2.id, n4.id, "risk_score <= 0.7");


      renderNodes();

      addLog("Loaded sample 'Hazard Review & Critical Escalation' workflow.");

    }


    /* Header Controls */

    function setupHeaderControls() {

      document.getElementById("btn-sample-flow").addEventListener("click", () => {

        loadSampleWorkflow();

        showToast("Loaded sample workflow.");

      });


      document.getElementById("btn-validate").addEventListener("click", () => {

        validatePipeline();

      });


      document.getElementById("btn-clear").addEventListener("click", () => {

        state.nodes = [];

        state.edges = [];

        clearSelections();

        renderNodes();

        showToast("Workflow canvas cleared.");

        addLog("Canvas cleared.", "warn");

      });


      document.getElementById("btn-run").addEventListener("click", () => {

        runSimulation();

      });


      // Speed controls

      const sSlow = document.getElementById("speed-slow");

      const sNorm = document.getElementById("speed-norm");

      const sFast = document.getElementById("speed-fast");


      sSlow.addEventListener("click", () => {

        state.executionSpeed = 1600;

        sSlow.className = "px-2 py-1 rounded bg-slate-800 text-cyan-400 font-semibold transition";

        sNorm.className = "px-2 py-1 rounded text-slate-400 hover:text-white transition";

        sFast.className = "px-2 py-1 rounded text-slate-400 hover:text-white transition";

      });


      sNorm.addEventListener("click", () => {

        state.executionSpeed = 900;

        sNorm.className = "px-2 py-1 rounded bg-slate-800 text-cyan-400 font-semibold transition";

        sSlow.className = "px-2 py-1 rounded text-slate-400 hover:text-white transition";

        sFast.className = "px-2 py-1 rounded text-slate-400 hover:text-white transition";

      });


      sFast.addEventListener("click", () => {

        state.executionSpeed = 400;

        sFast.className = "px-2 py-1 rounded bg-slate-800 text-cyan-400 font-semibold transition";

        sSlow.className = "px-2 py-1 rounded text-slate-400 hover:text-white transition";

        sNorm.className = "px-2 py-1 rounded text-slate-400 hover:text-white transition";

      });


      // Canvas Zoom Controls

      document.getElementById("btn-zoom-in").addEventListener("click", () => {

        state.zoom = Math.min(1.5, state.zoom + 0.1);

        applyZoom();

      });


      document.getElementById("btn-zoom-out").addEventListener("click", () => {

        state.zoom = Math.max(0.6, state.zoom - 0.1);

        applyZoom();

      });


      document.getElementById("btn-zoom-reset").addEventListener("click", () => {

        state.zoom = 1.0;

        applyZoom();

      });

    }


    function applyZoom() {

      nodesContainer.style.transform = `scale(${state.zoom})`;

      nodesContainer.style.transformOrigin = "top left";

      svgLayer.style.transform = `scale(${state.zoom})`;

      svgLayer.style.transformOrigin = "top left";

      document.getElementById("btn-zoom-reset").innerText = `${Math.round(state.zoom * 100)}%`;

    }


    /* ========================================================================

       8. SKILL FILE VIEWER MODAL

       ======================================================================== */

    let currentModalSkill = null;

    let currentModalTab = "SKILL.md";


    window.openSkillViewer = function(skillId) {

      const skill = SKILL_LIBRARY[skillId];

      if (!skill) return;


      currentModalSkill = skill;

      currentModalTab = "SKILL.md";


      document.getElementById("skill-modal-name").innerText = skill.id;

      document.getElementById("skill-modal-version").innerText = `v${skill.version}`;

      document.getElementById("skill-modal-desc").innerText = skill.description;


      updateSkillModalTabHighlight();

      renderSkillModalContent();

      modalSkillViewer.classList.remove("hidden");

    };


    function updateSkillModalTabHighlight() {

      const mdBtn = document.getElementById("skill-tab-md");

      const clsBtn = document.getElementById("skill-tab-classes");

      const execBtn = document.getElementById("skill-tab-executor");


      [mdBtn, clsBtn, execBtn].forEach(b => {

        b.className = "px-3 py-1.5 rounded-t border-t border-x border-transparent text-slate-400 hover:text-slate-200 flex items-center gap-1.5";

      });


      if (currentModalTab === "SKILL.md") {

        mdBtn.className = "px-3 py-1.5 rounded-t border-t border-x border-slate-700 bg-slate-900 text-cyan-400 flex items-center gap-1.5 font-bold";

      } else if (currentModalTab === "classes.py") {

        clsBtn.className = "px-3 py-1.5 rounded-t border-t border-x border-slate-700 bg-slate-900 text-yellow-400 flex items-center gap-1.5 font-bold";

      } else if (currentModalTab === "executor.py") {

        execBtn.className = "px-3 py-1.5 rounded-t border-t border-x border-slate-700 bg-slate-900 text-blue-400 flex items-center gap-1.5 font-bold";

      }

    }


    function renderSkillModalContent() {

      if (!currentModalSkill) return;

      const content = currentModalSkill.files[currentModalTab] || "# File not found";

      document.getElementById("skill-file-content").textContent = content;

    }


    document.getElementById("skill-tab-md").addEventListener("click", () => {

      currentModalTab = "SKILL.md";

      updateSkillModalTabHighlight();

      renderSkillModalContent();

    });


    document.getElementById("skill-tab-classes").addEventListener("click", () => {

      currentModalTab = "classes.py";

      updateSkillModalTabHighlight();

      renderSkillModalContent();

    });


    document.getElementById("skill-tab-executor").addEventListener("click", () => {

      currentModalTab = "executor.py";

      updateSkillModalTabHighlight();

      renderSkillModalContent();

    });


    document.getElementById("btn-close-skill-modal").addEventListener("click", () => modalSkillViewer.classList.add("hidden"));

    document.getElementById("btn-skill-modal-done").addEventListener("click", () => modalSkillViewer.classList.add("hidden"));


    // Launch

    window.onload = function() {

      init();

    };

  </script>

</body>

</html>


Comments

Popular posts from this blog

#4297 ISS Astronaut Unable to Speak

#3356 PHP and JavaScript to Retrieve and Set an API_Key

#4188 Hybrid/GraphRAG