| |
| """ |
| nima_agent_layer.py — OpenClaw-style self-authoring agent layer. |
| |
| Nima can create, use, and compose tools. When she doesn't have a tool |
| she needs, she writes one, tests it in a sandbox, and saves it to her |
| toolbox for later. |
| |
| NEUROBIOLOGICAL MAPPING: |
| ToolRegistry = basal ganglia + cerebellum (procedural memory) |
| ToolExecutor = primary motor cortex + spinal cord (action execution) |
| ToolPlanner = prefrontal cortex (executive planning) |
| ToolCreator = prefrontal ↔ motor integration (human-unique tool-making) |
| ToolCombiner = right hemisphere + PFC (creative synthesis) |
| ToolSandbox = cerebellum (safe practice space — test before using) |
| |
| ENVIRONMENT VARIABLES: |
| NIMA_TOOLS_DIR # where tool files live (default: ./tools/) |
| NIMA_SANDBOX_DIR # where sandbox execution happens (default: ./sandbox/) |
| NIMA_TOOL_AUTO_APPROVE # 1 = new tools don't need human approval |
| NIMA_TOOL_TIMEOUT_S # execution timeout (default: 30) |
| NIMA_TOOL_MEMORY_MB # memory limit (default: 256) |
| """ |
| from __future__ import annotations |
|
|
| import ast |
| import json |
| import logging |
| import os |
| import subprocess |
| import sys |
| import tempfile |
| import time |
| import uuid |
| from dataclasses import dataclass, field |
| from typing import Any, Dict, List, Optional, Tuple |
|
|
| logger = logging.getLogger("NimaAgent") |
|
|
| FORBIDDEN_MODULES = { |
| "os.system", "subprocess.Popen", "subprocess.call", "subprocess.run", |
| "os.popen", "commands.getoutput", |
| } |
| RESTRICTED_MODULES = { |
| "eval", "exec", "compile", |
| "os.remove", "os.rmdir", "os.unlink", "shutil.rmtree", |
| "socket", "http.client", "urllib.request", "requests", |
| "ctypes", "cffi", |
| } |
| ALLOWED_MODULES = { |
| "math", "statistics", "random", "itertools", "collections", |
| "json", "re", "string", "textwrap", "unicodedata", |
| "datetime", "calendar", "hashlib", "base64", |
| "fractions", "decimal", "numbers", |
| "typing", "dataclasses", "enum", |
| "functools", "operator", "pathlib", |
| "csv", "io", "tempfile", |
| } |
|
|
|
|
| @dataclass |
| class ToolMetadata: |
| tool_id: str |
| name: str |
| description: str |
| function_signature: str |
| file_path: str |
| created_at: float = field(default_factory=time.time) |
| created_by: str = "nima" |
| use_count: int = 0 |
| last_used: float = 0.0 |
| approved: bool = False |
| source_tools: List[str] = field(default_factory=list) |
| tags: List[str] = field(default_factory=list) |
| test_passed: bool = False |
|
|
| def to_dict(self) -> Dict[str, Any]: |
| return {k: v for k, v in self.__dict__.items()} |
|
|
|
|
| class ToolRegistry: |
| """JSON-indexed library of tool files — Nima's procedural memory.""" |
|
|
| def __init__(self, tools_dir: Optional[str] = None) -> None: |
| if tools_dir is None: |
| tools_dir = os.environ.get("NIMA_TOOLS_DIR", os.path.join(os.getcwd(), "tools")) |
| self.tools_dir = tools_dir |
| self._tools: Dict[str, ToolMetadata] = {} |
| self._lock = __import__("threading").Lock() |
| os.makedirs(self.tools_dir, exist_ok=True) |
| self._load() |
|
|
| def _registry_path(self) -> str: |
| return os.path.join(self.tools_dir, "_registry.json") |
|
|
| def _load(self) -> None: |
| path = self._registry_path() |
| if not os.path.exists(path): |
| return |
| try: |
| with open(path, "r") as f: |
| data = json.load(f) |
| for tid, md in data.items(): |
| self._tools[tid] = ToolMetadata(**md) |
| logger.info("[ToolRegistry] loaded %d tools", len(self._tools)) |
| except Exception as e: |
| logger.warning("[ToolRegistry] load failed: %s", e) |
|
|
| def _save(self) -> None: |
| path = self._registry_path() |
| try: |
| tmp = path + ".tmp" |
| with open(tmp, "w") as f: |
| json.dump({tid: t.to_dict() for tid, t in self._tools.items()}, |
| f, indent=2, default=str) |
| os.replace(tmp, path) |
| except Exception as e: |
| logger.warning("[ToolRegistry] save failed: %s", e) |
|
|
| def register(self, metadata: ToolMetadata) -> None: |
| with self._lock: |
| self._tools[metadata.tool_id] = metadata |
| self._save() |
|
|
| def get(self, tool_id: str) -> Optional[ToolMetadata]: |
| return self._tools.get(tool_id) |
|
|
| def find_by_name(self, name: str) -> Optional[ToolMetadata]: |
| for t in self._tools.values(): |
| if t.name == name: |
| return t |
| return None |
|
|
| def find_by_capability(self, description_query: str) -> List[ToolMetadata]: |
| query_lower = description_query.lower() |
| query_words = set(query_lower.split()) |
| results = [] |
| for t in self._tools.values(): |
| desc_lower = t.description.lower() |
| name_lower = t.name.lower() |
| tags_lower = [tag.lower() for tag in t.tags] |
| all_words = set() |
| all_words.update(desc_lower.split()) |
| all_words.update(name_lower.replace("_", " ").split()) |
| for tag in tags_lower: |
| all_words.update(tag.split()) |
| overlap = len(query_words & all_words) |
| for qw in query_words: |
| if len(qw) < 3: |
| continue |
| if qw in name_lower or qw in desc_lower: |
| overlap += 1 |
| continue |
| for tag in tags_lower: |
| if qw in tag: |
| overlap += 1 |
| break |
| else: |
| for word in all_words: |
| if word.startswith(qw) and len(word) > len(qw): |
| overlap += 1 |
| break |
| if overlap > 0: |
| results.append((t, overlap)) |
| results.sort(key=lambda x: -x[1]) |
| return [t for t, _ in results[:10]] |
|
|
| def list_all(self) -> List[ToolMetadata]: |
| return list(self._tools.values()) |
|
|
| def list_approved(self) -> List[ToolMetadata]: |
| return [t for t in self._tools.values() if t.approved] |
|
|
| def approve(self, tool_id: str) -> bool: |
| with self._lock: |
| t = self._tools.get(tool_id) |
| if t is None: |
| return False |
| t.approved = True |
| self._save() |
| return True |
|
|
| def record_use(self, tool_id: str) -> None: |
| with self._lock: |
| t = self._tools.get(tool_id) |
| if t is None: |
| return |
| t.use_count += 1 |
| t.last_used = time.time() |
| self._save() |
|
|
| def get_stats(self) -> Dict[str, Any]: |
| return { |
| "total_tools": len(self._tools), |
| "approved_tools": sum(1 for t in self._tools.values() if t.approved), |
| "self_authored": sum(1 for t in self._tools.values() if t.created_by == "nima"), |
| "total_uses": sum(t.use_count for t in self._tools.values()), |
| } |
|
|
|
|
| class ToolSandbox: |
| """Safe execution environment for tool code.""" |
|
|
| def __init__(self, sandbox_dir: Optional[str] = None) -> None: |
| if sandbox_dir is None: |
| sandbox_dir = os.environ.get("NIMA_SANDBOX_DIR", os.path.join(os.getcwd(), "sandbox")) |
| self.sandbox_dir = sandbox_dir |
| os.makedirs(self.sandbox_dir, exist_ok=True) |
| self.timeout_s = float(os.environ.get("NIMA_TOOL_TIMEOUT_S", "30")) |
| self.memory_limit_mb = float(os.environ.get("NIMA_TOOL_MEMORY_MB", "256")) |
|
|
| def scan_code(self, code: str) -> Tuple[bool, List[str]]: |
| issues: List[str] = [] |
| try: |
| tree = ast.parse(code) |
| except SyntaxError as e: |
| return False, [f"SyntaxError: {e}"] |
| for node in ast.walk(tree): |
| if isinstance(node, ast.Import): |
| for alias in node.names: |
| mod = alias.name |
| if mod in FORBIDDEN_MODULES: |
| issues.append(f"Forbidden import: {mod}") |
| elif mod in RESTRICTED_MODULES: |
| issues.append(f"Restricted import (needs approval): {mod}") |
| elif isinstance(node, ast.ImportFrom): |
| mod = node.module or "" |
| if mod in FORBIDDEN_MODULES: |
| issues.append(f"Forbidden import: {mod}") |
| elif mod in RESTRICTED_MODULES: |
| issues.append(f"Restricted import (needs approval): {mod}") |
| blocked = [i for i in issues if "Forbidden" in i or "Restricted" in i] |
| return len(blocked) == 0, issues |
|
|
| def _build_runner_code(self, tool_file: str, function_name: str) -> str: |
| """Build the runner script that executes a tool in a subprocess. |
| |
| Includes memory limiting via resource.setrlimit where supported. |
| """ |
| memory_bytes = int(self.memory_limit_mb * 1024 * 1024) |
| runner_code = ( |
| f"import sys, json, importlib.util, resource\n" |
| f"# Enforce memory limit (Linux/macOS only)\n" |
| f"try:\n" |
| f" resource.setrlimit(resource.RLIMIT_AS, ({memory_bytes}, {memory_bytes}))\n" |
| f"except (ValueError, AttributeError):\n" |
| f" pass # not available on this platform\n" |
| f"spec = importlib.util.spec_from_file_location('tool', {tool_file!r})\n" |
| f"mod = importlib.util.module_from_spec(spec)\n" |
| f"spec.loader.exec_module(mod)\n" |
| f"fn = getattr(mod, {function_name!r})\n" |
| f"args = json.loads(sys.argv[1])\n" |
| f"kwargs = json.loads(sys.argv[2])\n" |
| f"try:\n" |
| f" result = fn(*args, **kwargs)\n" |
| f" print('__RESULT__' + json.dumps({{'result': result}}, default=str))\n" |
| f"except Exception as e:\n" |
| f" print('__ERROR__' + json.dumps({{'error': str(e)}}))\n" |
| ) |
| return runner_code |
|
|
| def execute(self, code: str, function_name: str, |
| args: List[Any] = None, kwargs: Dict[str, Any] = None, |
| ) -> Dict[str, Any]: |
| args = args or [] |
| kwargs = kwargs or {} |
| t0 = time.time() |
| passes, issues = self.scan_code(code) |
| blocked = [i for i in issues if "Forbidden" in i or "Restricted" in i] |
| if blocked: |
| return {"success": False, "result": None, "stderr": "Blocked by safety scan", |
| "execution_time_ms": 0, "issues": blocked} |
| tool_file = os.path.join(self.sandbox_dir, f"_tool_{uuid.uuid4().hex[:8]}.py") |
| runner_file = os.path.join(self.sandbox_dir, f"_runner_{uuid.uuid4().hex[:8]}.py") |
| try: |
| with open(tool_file, "w") as f: |
| f.write(code) |
| runner_code = self._build_runner_code(tool_file, function_name) |
| with open(runner_file, "w") as f: |
| f.write(runner_code) |
| proc = subprocess.run( |
| [sys.executable, runner_file, json.dumps(args), json.dumps(kwargs)], |
| capture_output=True, text=True, timeout=self.timeout_s, cwd=self.sandbox_dir, |
| ) |
| elapsed_ms = (time.time() - t0) * 1000 |
| stdout, stderr = proc.stdout, proc.stderr |
| result = None |
| success = False |
| for marker in ("__RESULT__", "__ERROR__"): |
| if marker in stdout: |
| try: |
| payload = json.loads(stdout.split(marker)[-1].strip()) |
| if marker == "__RESULT__": |
| result = payload.get("result") |
| success = True |
| else: |
| stderr = payload.get("error", stderr) |
| except Exception: |
| pass |
| break |
| return {"success": success, "result": result, |
| "stdout": stdout[:2000], "stderr": stderr[:2000], |
| "execution_time_ms": round(elapsed_ms, 1), "issues": issues} |
| except subprocess.TimeoutExpired: |
| return {"success": False, "result": None, "stdout": "", |
| "stderr": f"Timeout after {self.timeout_s}s", |
| "execution_time_ms": round((time.time() - t0) * 1000, 1), "issues": issues} |
| except Exception as e: |
| return {"success": False, "result": None, "stdout": "", |
| "stderr": str(e), "execution_time_ms": 0, "issues": issues} |
| finally: |
| for p in (tool_file, runner_file): |
| try: |
| os.remove(p) |
| except Exception: |
| pass |
|
|
|
|
| class ToolExecutor: |
| """Executes approved tools from the registry.""" |
| def __init__(self, registry: ToolRegistry, sandbox: ToolSandbox) -> None: |
| self.registry = registry |
| self.sandbox = sandbox |
|
|
| def execute(self, tool_id: str, args: List[Any] = None, |
| kwargs: Dict[str, Any] = None) -> Dict[str, Any]: |
| tool = self.registry.get(tool_id) |
| if tool is None: |
| return {"success": False, "error": f"Tool not found: {tool_id}"} |
| if not tool.approved: |
| return {"success": False, "error": f"Tool not approved: {tool.name}"} |
| tool_path = os.path.join(self.registry.tools_dir, tool.file_path) |
| if not os.path.exists(tool_path): |
| return {"success": False, "error": f"Tool file missing: {tool_path}"} |
| with open(tool_path, "r") as f: |
| code = f.read() |
| result = self.sandbox.execute(code, tool.name, args, kwargs) |
| if result["success"]: |
| self.registry.record_use(tool_id) |
| return result |
|
|
|
|
| class ToolPlanner: |
| """Decides which tool to call for a given task.""" |
| def __init__(self, registry: ToolRegistry) -> None: |
| self.registry = registry |
|
|
| MAX_PLAN_TOOLS = 3 |
|
|
| def plan(self, task_description: str) -> Dict[str, Any]: |
| candidates = self.registry.find_by_capability(task_description) |
| approved = [t for t in candidates if t.approved] |
| if not approved: |
| return {"needs_tool": False, "tools": [], |
| "reasoning": "No approved tools match."} |
| top_n = approved[:self.MAX_PLAN_TOOLS] |
| return {"needs_tool": True, "tools": [{ |
| "tool_id": t.tool_id, "name": t.name, |
| "args": [], "kwargs": {}, |
| "reason": f"Candidate for: {task_description[:80]}", |
| } for t in top_n], "reasoning": f"Selected {len(top_n)} tool(s): {', '.join(t.name for t in top_n)}"} |
|
|
|
|
| class ToolCreator: |
| """When Nima doesn't have a tool she needs, she writes one.""" |
| TOOL_TEMPLATE = '''"""Tool: {name} — {description}""" |
| import math, json, re |
| from typing import Any, Dict, List, Optional |
| |
| def {function_name}({signature}): |
| """{description}""" |
| # TODO: Nima will fill this in via LLM |
| pass |
| ''' |
|
|
| def __init__(self, registry: ToolRegistry, sandbox: ToolSandbox, |
| llm_provider: Optional[Any] = None) -> None: |
| self.registry = registry |
| self.sandbox = sandbox |
| self.llm_provider = llm_provider |
| self.auto_approve = bool( |
| os.environ.get("NIMA_TOOL_AUTO_APPROVE", "0") in ("1", "true", "True")) |
|
|
| def create_tool(self, name: str, description: str, function_signature: str, |
| test_cases: Optional[List[Dict[str, Any]]] = None) -> Dict[str, Any]: |
| code = self._generate_code(name, description, function_signature, test_cases) |
| test_results = [] |
| all_passed = True |
| if test_cases: |
| for i, tc in enumerate(test_cases): |
| result = self.sandbox.execute(code, name, tc.get("args", []), tc.get("kwargs", {})) |
| passed = result["success"] |
| if passed and "expected" in tc: |
| passed = result["result"] == tc["expected"] |
| test_results.append({"case": i, "passed": passed, |
| "result": result.get("result"), |
| "expected": tc.get("expected")}) |
| if not passed: |
| all_passed = False |
| else: |
| passes_scan, issues = self.sandbox.scan_code(code) |
| all_passed = passes_scan |
| test_results = [{"case": 0, "passed": passes_scan, "issues": issues}] |
| approved = all_passed and self.auto_approve |
| tool_id = f"tool_{uuid.uuid4().hex[:8]}" |
| file_name = f"{name}.py" |
| file_path = os.path.join(self.registry.tools_dir, file_name) |
| with open(file_path, "w") as f: |
| f.write(code) |
| metadata = ToolMetadata( |
| tool_id=tool_id, name=name, description=description, |
| function_signature=f"{name}({function_signature})", file_path=file_name, |
| created_by="nima", approved=approved, test_passed=all_passed, |
| tags=self._extract_tags(description), |
| ) |
| self.registry.register(metadata) |
| return {"success": True, "tool_id": tool_id, "approved": approved, |
| "test_results": test_results} |
|
|
| def _generate_code(self, name, description, signature, test_cases=None) -> str: |
| if self.llm_provider and getattr(self.llm_provider, "available", False): |
| try: |
| system_prompt = ("You are Nima's tool-creation subsystem. Write a single " |
| "Python function. Use only safe stdlib modules. Output ONLY Python code.") |
| user_prompt = f"Tool: {name}\nDescription: {description}\nSignature: def {name}({signature}):" |
| code = self.llm_provider.generate( |
| system_prompt=system_prompt, |
| messages=[{"role": "user", "content": user_prompt}], |
| temperature=0.2, max_tokens=1024, |
| ) |
| if code.startswith("```"): |
| lines = code.split("\n")[1:] |
| if lines and lines[-1].strip() == "```": |
| lines = lines[:-1] |
| code = "\n".join(lines) |
| return code |
| except Exception: |
| pass |
| return self.TOOL_TEMPLATE.format( |
| name=name, description=description, function_name=name, signature=signature, |
| ) |
|
|
| def _extract_tags(self, description: str) -> List[str]: |
| tags = [] |
| desc_lower = description.lower() |
| if any(w in desc_lower for w in ["search", "find", "lookup"]): |
| tags.append("search") |
| if any(w in desc_lower for w in ["file", "read", "write"]): |
| tags.append("file") |
| if any(w in desc_lower for w in ["math", "calculate", "compute", "convert"]): |
| tags.append("math") |
| if any(w in desc_lower for w in ["web", "url"]): |
| tags.append("web") |
| if any(w in desc_lower for w in ["time", "date"]): |
| tags.append("time") |
| return tags or ["general"] |
|
|
|
|
| class ToolCombiner: |
| """Composes existing tools into composite tools.""" |
| def __init__(self, registry: ToolRegistry, creator: ToolCreator) -> None: |
| self.registry = registry |
| self.creator = creator |
|
|
| def combine(self, name: str, description: str, |
| source_tool_ids: List[str], combination_logic: str) -> Dict[str, Any]: |
| source_tools = [] |
| for tid in source_tool_ids: |
| t = self.registry.get(tid) |
| if t is None: |
| return {"success": False, "error": f"Source tool not found: {tid}"} |
| source_tools.append(t) |
| full_desc = f"{description}\nCombines: {', '.join(t.name for t in source_tools)}\nLogic: {combination_logic}" |
| result = self.creator.create_tool(name=name, description=full_desc, |
| function_signature="*args, **kwargs") |
| if result.get("success"): |
| tool = self.registry.get(result["tool_id"]) |
| if tool: |
| tool.source_tools = source_tool_ids |
| tool.tags.append("composite") |
| self.registry.register(tool) |
| return result |
|
|
|
|
| class AgentLayer: |
| """Facade that ties together the full agent layer.""" |
| def __init__(self, tools_dir: Optional[str] = None, |
| sandbox_dir: Optional[str] = None, |
| llm_provider: Optional[Any] = None) -> None: |
| self.registry = ToolRegistry(tools_dir) |
| self.sandbox = ToolSandbox(sandbox_dir) |
| self.executor = ToolExecutor(self.registry, self.sandbox) |
| self.planner = ToolPlanner(self.registry) |
| self.creator = ToolCreator(self.registry, self.sandbox, llm_provider) |
| self.combiner = ToolCombiner(self.registry, self.creator) |
|
|
| def register_starter_tools(self) -> None: |
| starter_code = { |
| "calculator": '''"""Tool: calculator — Evaluate a mathematical expression safely.""" |
| import ast, operator as op |
| _OPS = {ast.Add: op.add, ast.Sub: op.sub, ast.Mult: op.mul, ast.Div: op.truediv, |
| ast.Pow: op.pow, ast.Mod: op.mod, ast.USub: op.neg, ast.UAdd: op.pos, ast.FloorDiv: op.floordiv} |
| def calculator(expression: str): |
| """Evaluate a mathematical expression. Returns float.""" |
| def _eval(node): |
| if isinstance(node, ast.Num): return node.n |
| if isinstance(node, ast.Constant): return node.value |
| if isinstance(node, ast.BinOp): return _OPS[type(node.op)](_eval(node.left), _eval(node.right)) |
| if isinstance(node, ast.UnaryOp): return _OPS[type(node.op)](_eval(node.operand)) |
| raise ValueError(f"Unsupported: {type(node).__name__}") |
| tree = ast.parse(expression, mode="eval") |
| return float(_eval(tree.body)) |
| ''', |
| "text_stats": '''"""Tool: text_stats — Compute statistics about text.""" |
| import re |
| from collections import Counter |
| def text_stats(text: str): |
| """Compute word count, char count, sentence count, avg word length, top words.""" |
| words = re.findall(r"\\b\\w+\\b", text.lower()) |
| sentences = [s for s in re.split(r"[.!?]+", text) if s.strip()] |
| word_lens = [len(w) for w in words] |
| return {"word_count": len(words), "char_count": len(text), |
| "sentence_count": len(sentences), |
| "avg_word_length": sum(word_lens)/max(1,len(word_lens)), |
| "top_words": Counter(words).most_common(5)} |
| ''', |
| "time_now": '''"""Tool: time_now — Get the current time and date.""" |
| import time |
| from datetime import datetime |
| def time_now(): |
| """Get current time and date. Returns dict with iso, date, time, weekday.""" |
| now = datetime.now() |
| return {"iso": now.isoformat(), "unix": time.time(), |
| "date": now.strftime("%Y-%m-%d"), "time": now.strftime("%H:%M:%S"), |
| "weekday": now.strftime("%A")} |
| ''', |
| } |
| for name, code in starter_code.items(): |
| if self.registry.find_by_name(name) is not None: |
| continue |
| file_path = os.path.join(self.registry.tools_dir, f"{name}.py") |
| with open(file_path, "w") as f: |
| f.write(code) |
| result = self.sandbox.execute(code, name, [], {}) |
| desc = code.split("—")[1].split('"""')[0].strip() if "—" in code else name |
| metadata = ToolMetadata( |
| tool_id=f"tool_{uuid.uuid4().hex[:8]}", name=name, |
| description=desc, function_signature=f"{name}()", file_path=f"{name}.py", |
| created_by="human", approved=True, test_passed=result["success"], |
| tags=["built-in", name], |
| ) |
| self.registry.register(metadata) |
|
|
| def get_stats(self) -> Dict[str, Any]: |
| return {"registry": self.registry.get_stats(), |
| "sandbox_timeout_s": self.sandbox.timeout_s, |
| "auto_approve": self.creator.auto_approve} |
|
|
|
|
| if __name__ == "__main__": |
| logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") |
| import tempfile |
| tmp_tools = tempfile.mkdtemp(prefix="nima_tools_test_") |
| tmp_sandbox = tempfile.mkdtemp(prefix="nima_sandbox_test_") |
| os.environ["NIMA_TOOLS_DIR"] = tmp_tools |
| os.environ["NIMA_SANDBOX_DIR"] = tmp_sandbox |
| os.environ["NIMA_TOOL_AUTO_APPROVE"] = "1" |
| agent = AgentLayer() |
| agent.register_starter_tools() |
| calc = agent.registry.find_by_name("calculator") |
| if calc: |
| r = agent.executor.execute(calc.tool_id, args=["2 + 3 * 4"]) |
| print(f"Calculator: 2 + 3 * 4 = {r.get('result')} (expected 14.0)") |
| ts = agent.registry.find_by_name("text_stats") |
| if ts: |
| r = agent.executor.execute(ts.tool_id, args=["Hello world. Testing."]) |
| print(f"Text stats: {r.get('result')}") |
| tn = agent.registry.find_by_name("time_now") |
| if tn: |
| r = agent.executor.execute(tn.tool_id, args=[]) |
| print(f"Time: {r.get('result', {}).get('iso', '?')}") |
| print(f"Stats: {json.dumps(agent.get_stats(), indent=2)}") |
| import shutil |
| shutil.rmtree(tmp_tools, ignore_errors=True) |
| shutil.rmtree(tmp_sandbox, ignore_errors=True) |
| print("\nAgent layer test PASSED") |
|
|