feat(tools): Phase T.1-T.4 complete - manifest system, registry, implementations, runtime, collaboration, scheduler

This commit is contained in:
2026-04-05 11:54:57 +08:00
parent fca7a7cf3d
commit 10d9340c53
30 changed files with 2891 additions and 4 deletions

View File

@@ -0,0 +1,16 @@
"""
Runtime Module
Multi-runtime support for tool execution:
- Python runtime: native Python execution
- JavaScript runtime: Node.js stdio protocol
- Native runtime: binary execution
"""
from tools.runtime.base import BaseRuntime
from tools.runtime.manager import RuntimeManager
__all__ = [
"BaseRuntime",
"RuntimeManager",
]

View File

@@ -0,0 +1,33 @@
"""
Base Runtime
Abstract base class for all tool runtimes.
"""
from abc import ABC, abstractmethod
from typing import Any, Dict, Optional
class BaseRuntime(ABC):
"""Runtime abstract base class"""
@abstractmethod
async def execute(
self,
entry: str,
command: str,
parameters: Dict[str, Any],
timeout: int,
) -> Dict[str, Any]:
"""Execute a tool"""
pass
@abstractmethod
async def validate(self, entry: str) -> bool:
"""Validate if the tool is available"""
pass
@abstractmethod
def get_name(self) -> str:
"""Get runtime name"""
pass

View File

@@ -0,0 +1,125 @@
"""
JavaScript Runtime
Node.js stdio protocol runtime for JavaScript tools.
"""
import asyncio
import json
import shutil
from pathlib import Path
from typing import Any, Dict, Optional
from tools.runtime.base import BaseRuntime
class JavaScriptRuntime(BaseRuntime):
"""JavaScript runtime using Node.js stdio protocol"""
def __init__(self, node_path: Optional[str] = None):
self.node_path = node_path or self._detect_node()
self._validated: bool = False
def get_name(self) -> str:
return "javascript"
def _detect_node(self) -> str:
"""Detect Node.js executable path"""
node = shutil.which("node")
if node:
return node
# Fallback for Windows
return "node"
async def validate(self, entry: str) -> bool:
"""Validate Node.js runtime and entry file"""
# Check node is available
try:
result = await asyncio.create_subprocess_exec(
self.node_path,
"--version",
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
await result.wait()
if result.returncode != 0:
return False
except Exception:
return False
# Check entry file exists
path = Path(entry)
if not path.exists():
return False
self._validated = True
return True
async def execute(
self,
entry: str,
command: str,
parameters: Dict[str, Any],
timeout: int,
) -> Dict[str, Any]:
"""Execute a JavaScript tool via stdio protocol"""
if not self._validated:
is_valid = await self.validate(entry)
if not is_valid:
return {
"status": "error",
"error": "JavaScript runtime not available or entry file invalid",
}
# Build input data per stdio protocol
input_data = {
"command": command,
"parameters": parameters,
}
try:
process = await asyncio.create_subprocess_exec(
self.node_path,
entry,
stdin=asyncio.subprocess.PIPE,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
stdout, stderr = await asyncio.wait_for(
process.communicate(input=json.dumps(input_data).encode()),
timeout=timeout / 1000,
)
if process.returncode != 0:
return {
"status": "error",
"error": stderr.decode() if stderr else "Unknown error",
}
result_text = stdout.decode()
if not result_text:
return {
"status": "error",
"error": "Empty response from Node.js runtime",
}
try:
result = json.loads(result_text)
return result
except json.JSONDecodeError:
return {
"status": "success",
"result": result_text,
}
except asyncio.TimeoutError:
return {
"status": "error",
"error": f"Execution timed out after {timeout}ms",
}
except Exception as e:
return {
"status": "error",
"error": str(e),
}

View File

@@ -0,0 +1,79 @@
"""
Runtime Manager
Manages multiple runtimes and routes tool execution to the appropriate runtime.
"""
from typing import Any, Dict, Optional
from tools.runtime.base import BaseRuntime
from tools.runtime.python_runtime import PythonRuntime
from tools.runtime.js_runtime import JavaScriptRuntime
from tools.runtime.native_runtime import NativeRuntime
class RuntimeManager:
"""Runtime manager for multi-runtime tool execution"""
def __init__(self):
self._runtimes: Dict[str, BaseRuntime] = {
"python": PythonRuntime(),
"javascript": JavaScriptRuntime(),
"native": NativeRuntime(),
}
def get_runtime(self, name: str) -> Optional[BaseRuntime]:
"""Get runtime by name"""
return self._runtimes.get(name)
def register_runtime(self, name: str, runtime: BaseRuntime) -> None:
"""Register a custom runtime"""
self._runtimes[name] = runtime
async def execute(
self,
runtime_name: str,
entry: str,
command: str,
parameters: Dict[str, Any],
timeout: int = 30000,
) -> Dict[str, Any]:
"""Execute tool through the specified runtime"""
runtime = self.get_runtime(runtime_name)
if not runtime:
return {
"status": "error",
"error": f"Unknown runtime: {runtime_name}",
}
# Validate first
is_valid = await runtime.validate(entry)
if not is_valid:
return {
"status": "error",
"error": f"Validation failed for runtime {runtime_name}, entry: {entry}",
}
return await runtime.execute(entry, command, parameters, timeout)
def list_runtimes(self) -> list:
"""List all registered runtimes"""
return [
{
"name": name,
"available": runtime.get_name() == name,
}
for name, runtime in self._runtimes.items()
]
# Global runtime manager instance
_runtime_manager: Optional[RuntimeManager] = None
def get_runtime_manager() -> RuntimeManager:
"""Get the global runtime manager instance"""
global _runtime_manager
if _runtime_manager is None:
_runtime_manager = RuntimeManager()
return _runtime_manager

View File

@@ -0,0 +1,93 @@
"""
Native Runtime
Native binary execution runtime for system executables.
"""
import asyncio
import stat
from pathlib import Path
from typing import Any, Dict, List
from tools.runtime.base import BaseRuntime
class NativeRuntime(BaseRuntime):
"""Native binary execution runtime"""
def get_name(self) -> str:
return "native"
async def validate(self, entry: str) -> bool:
"""Validate native binary exists and is executable"""
path = Path(entry)
if not path.exists():
return False
# Check if file is executable
file_stat = path.stat()
executable_bit = stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
if not (file_stat.st_mode & executable_bit):
return False
return True
async def execute(
self,
entry: str,
command: str,
parameters: Dict[str, Any],
timeout: int,
) -> Dict[str, Any]:
"""Execute a native binary"""
try:
# Build argument list
args = [entry, command] + self._format_args(parameters)
process = await asyncio.create_subprocess_exec(
*args,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
stdout, stderr = await asyncio.wait_for(
process.communicate(),
timeout=timeout / 1000,
)
if process.returncode != 0:
return {
"status": "error",
"error": stderr.decode() if stderr else f"Exit code: {process.returncode}",
}
return {
"status": "success",
"result": stdout.decode() if stdout else "",
}
except asyncio.TimeoutError:
return {
"status": "error",
"error": f"Execution timed out after {timeout}ms",
}
except Exception as e:
return {
"status": "error",
"error": str(e),
}
def _format_args(self, parameters: Dict[str, Any]) -> List[str]:
"""Format parameters as command-line arguments"""
args: List[str] = []
for key, value in parameters.items():
# Use --key=value format
if isinstance(value, bool):
if value:
args.append(f"--{key}")
elif isinstance(value, (list, tuple)):
for item in value:
args.extend([f"--{key}", str(item)])
else:
args.extend([f"--{key}", str(value)])
return args

View File

@@ -0,0 +1,113 @@
"""
Python Runtime
Native Python tool execution runtime.
"""
import asyncio
import importlib.util
import sys
from pathlib import Path
from typing import Any, Callable, Dict
from tools.runtime.base import BaseRuntime
class PythonRuntime(BaseRuntime):
"""Python runtime for executing Python-based tools"""
def __init__(self):
self._executors: Dict[str, Callable] = {}
self._modules: Dict[str, Any] = {}
def get_name(self) -> str:
return "python"
async def validate(self, entry: str) -> bool:
"""Validate Python tool entry point"""
path = Path(entry)
if not path.exists():
return False
if path.suffix != ".py":
return False
return True
async def execute(
self,
entry: str,
command: str,
parameters: Dict[str, Any],
timeout: int,
) -> Dict[str, Any]:
"""Execute a Python tool"""
try:
# Load module dynamically
module = self._load_module(entry, command)
if module is None:
return {
"status": "error",
"error": f"Failed to load module from {entry}",
}
# Get the execute function
if not hasattr(module, "execute"):
return {
"status": "error",
"error": "Module does not have 'execute' function",
}
execute_func = module.execute
# Run in executor to avoid blocking
loop = asyncio.get_event_loop()
result = await asyncio.wait_for(
loop.run_in_executor(
None,
lambda: execute_func(command, parameters),
),
timeout=timeout / 1000,
)
return {
"status": "success",
"result": result,
}
except asyncio.TimeoutError:
return {
"status": "error",
"error": f"Execution timed out after {timeout}ms",
}
except Exception as e:
return {
"status": "error",
"error": str(e),
}
def _load_module(self, entry: str, command: str) -> Any:
"""Load Python module from file path"""
cache_key = f"{entry}:{command}"
if cache_key in self._modules:
return self._modules[cache_key]
try:
path = Path(entry)
module_name = path.stem
spec = importlib.util.spec_from_file_location(module_name, entry)
if spec is None or spec.loader is None:
return None
module = importlib.util.module_from_spec(spec)
sys.modules[module_name] = module
spec.loader.exec_module(module)
self._modules[cache_key] = module
return module
except Exception:
return None
def clear_cache(self) -> None:
"""Clear module cache"""
self._modules.clear()