307 lines
10 KiB
Python
307 lines
10 KiB
Python
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
from datetime import datetime
|
||
|
|
from pathlib import Path
|
||
|
|
import platform
|
||
|
|
import subprocess
|
||
|
|
import sys
|
||
|
|
from typing import Any, Dict, List
|
||
|
|
|
||
|
|
from .registry import ToolContext, ToolRegistry
|
||
|
|
|
||
|
|
|
||
|
|
def build_default_registry() -> ToolRegistry:
|
||
|
|
registry = ToolRegistry()
|
||
|
|
registry.register(
|
||
|
|
name="current_time",
|
||
|
|
description="Get the current local time for the runtime.",
|
||
|
|
parameters={"type": "object", "properties": {}, "additionalProperties": False},
|
||
|
|
handler=_current_time,
|
||
|
|
)
|
||
|
|
registry.register(
|
||
|
|
name="memory_add",
|
||
|
|
description="Save a durable memory entry for future turns and sessions.",
|
||
|
|
parameters={
|
||
|
|
"type": "object",
|
||
|
|
"properties": {
|
||
|
|
"content": {"type": "string"},
|
||
|
|
"kind": {"type": "string", "enum": ["memory", "preference", "project"]},
|
||
|
|
},
|
||
|
|
"required": ["content"],
|
||
|
|
"additionalProperties": False,
|
||
|
|
},
|
||
|
|
handler=_memory_add,
|
||
|
|
)
|
||
|
|
registry.register(
|
||
|
|
name="memory_list",
|
||
|
|
description="List saved memory entries.",
|
||
|
|
parameters={
|
||
|
|
"type": "object",
|
||
|
|
"properties": {
|
||
|
|
"kind": {"type": "string", "enum": ["memory", "preference", "project"]},
|
||
|
|
},
|
||
|
|
"additionalProperties": False,
|
||
|
|
},
|
||
|
|
handler=_memory_list,
|
||
|
|
)
|
||
|
|
registry.register(
|
||
|
|
name="list_skills",
|
||
|
|
description="List installed skills with name and description.",
|
||
|
|
parameters={"type": "object", "properties": {}, "additionalProperties": False},
|
||
|
|
handler=_list_skills,
|
||
|
|
)
|
||
|
|
registry.register(
|
||
|
|
name="load_skill",
|
||
|
|
description="Load a skill into the active session by skill name.",
|
||
|
|
parameters={
|
||
|
|
"type": "object",
|
||
|
|
"properties": {
|
||
|
|
"name": {"type": "string", "description": "Skill name to activate."},
|
||
|
|
},
|
||
|
|
"required": ["name"],
|
||
|
|
"additionalProperties": False,
|
||
|
|
},
|
||
|
|
handler=_load_skill,
|
||
|
|
)
|
||
|
|
registry.register(
|
||
|
|
name="dispatch_task",
|
||
|
|
description="Create one or more subtasks for the current goal.",
|
||
|
|
parameters={
|
||
|
|
"type": "object",
|
||
|
|
"properties": {
|
||
|
|
"tasks": {
|
||
|
|
"type": "array",
|
||
|
|
"items": {
|
||
|
|
"type": "object",
|
||
|
|
"properties": {
|
||
|
|
"title": {"type": "string"},
|
||
|
|
"description": {"type": "string"},
|
||
|
|
"depends_on": {"type": "array", "items": {"type": "string"}},
|
||
|
|
"assignee": {"type": "string"},
|
||
|
|
},
|
||
|
|
"required": ["title", "description"],
|
||
|
|
"additionalProperties": False,
|
||
|
|
},
|
||
|
|
}
|
||
|
|
},
|
||
|
|
"required": ["tasks"],
|
||
|
|
"additionalProperties": False,
|
||
|
|
},
|
||
|
|
handler=_dispatch_task,
|
||
|
|
)
|
||
|
|
registry.register(
|
||
|
|
name="list_tasks",
|
||
|
|
description="List the current task board.",
|
||
|
|
parameters={"type": "object", "properties": {}, "additionalProperties": False},
|
||
|
|
handler=_list_tasks,
|
||
|
|
)
|
||
|
|
registry.register(
|
||
|
|
name="update_task",
|
||
|
|
description="Update task status or metadata.",
|
||
|
|
parameters={
|
||
|
|
"type": "object",
|
||
|
|
"properties": {
|
||
|
|
"task_id": {"type": "string"},
|
||
|
|
"status": {
|
||
|
|
"type": "string",
|
||
|
|
"enum": ["pending", "ready", "running", "blocked", "done"],
|
||
|
|
},
|
||
|
|
"notes": {"type": "string"},
|
||
|
|
},
|
||
|
|
"required": ["task_id"],
|
||
|
|
"additionalProperties": False,
|
||
|
|
},
|
||
|
|
handler=_update_task,
|
||
|
|
)
|
||
|
|
registry.register(
|
||
|
|
name="read_file",
|
||
|
|
description="Read a UTF-8 text file inside the workspace only.",
|
||
|
|
parameters={
|
||
|
|
"type": "object",
|
||
|
|
"properties": {"path": {"type": "string"}},
|
||
|
|
"required": ["path"],
|
||
|
|
"additionalProperties": False,
|
||
|
|
},
|
||
|
|
handler=_read_file,
|
||
|
|
)
|
||
|
|
registry.register(
|
||
|
|
name="execute_shell",
|
||
|
|
description="Run a shell command for environment inspection or task execution. Use this for OS, hardware, process, disk, and command-line checks.",
|
||
|
|
parameters={
|
||
|
|
"type": "object",
|
||
|
|
"properties": {
|
||
|
|
"command": {"type": "string"},
|
||
|
|
"timeout_seconds": {"type": "integer", "minimum": 1, "maximum": 120},
|
||
|
|
},
|
||
|
|
"required": ["command"],
|
||
|
|
"additionalProperties": False,
|
||
|
|
},
|
||
|
|
handler=_execute_shell,
|
||
|
|
)
|
||
|
|
registry.register(
|
||
|
|
name="run_python",
|
||
|
|
description="Run a short Python snippet when no built-in tool directly solves the task. Prefer printing concise results.",
|
||
|
|
parameters={
|
||
|
|
"type": "object",
|
||
|
|
"properties": {
|
||
|
|
"code": {"type": "string"},
|
||
|
|
"timeout_seconds": {"type": "integer", "minimum": 1, "maximum": 120},
|
||
|
|
},
|
||
|
|
"required": ["code"],
|
||
|
|
"additionalProperties": False,
|
||
|
|
},
|
||
|
|
handler=_run_python,
|
||
|
|
)
|
||
|
|
registry.register(
|
||
|
|
name="write_file",
|
||
|
|
description="Write a UTF-8 text file inside the workspace.",
|
||
|
|
parameters={
|
||
|
|
"type": "object",
|
||
|
|
"properties": {
|
||
|
|
"path": {"type": "string"},
|
||
|
|
"content": {"type": "string"},
|
||
|
|
},
|
||
|
|
"required": ["path", "content"],
|
||
|
|
"additionalProperties": False,
|
||
|
|
},
|
||
|
|
handler=_write_file,
|
||
|
|
)
|
||
|
|
return registry
|
||
|
|
|
||
|
|
|
||
|
|
def _current_time(ctx: ToolContext, args: Dict[str, Any]) -> Dict[str, Any]:
|
||
|
|
return {"success": True, "current_time": datetime.now().isoformat()}
|
||
|
|
|
||
|
|
|
||
|
|
def _memory_add(ctx: ToolContext, args: Dict[str, Any]) -> Dict[str, Any]:
|
||
|
|
if ctx.memory_store is None:
|
||
|
|
return {"success": False, "error": "memory store is not configured"}
|
||
|
|
entry = ctx.memory_store.add(args["content"], kind=args.get("kind", "memory"))
|
||
|
|
return {"success": True, "entry": {"id": entry.id, "kind": entry.kind, "content": entry.content}}
|
||
|
|
|
||
|
|
|
||
|
|
def _memory_list(ctx: ToolContext, args: Dict[str, Any]) -> Dict[str, Any]:
|
||
|
|
if ctx.memory_store is None:
|
||
|
|
return {"success": False, "error": "memory store is not configured"}
|
||
|
|
return {"success": True, "entries": ctx.memory_store.list_entries(kind=args.get("kind"))}
|
||
|
|
|
||
|
|
|
||
|
|
def _list_skills(ctx: ToolContext, args: Dict[str, Any]) -> Dict[str, Any]:
|
||
|
|
return {"success": True, "skills": ctx.skill_store.list_skills()}
|
||
|
|
|
||
|
|
|
||
|
|
def _load_skill(ctx: ToolContext, args: Dict[str, Any]) -> Dict[str, Any]:
|
||
|
|
skill = ctx.skill_store.load_skill(args["name"])
|
||
|
|
active = ctx.session.setdefault("active_skills", [])
|
||
|
|
if skill.name not in active:
|
||
|
|
active.append(skill.name)
|
||
|
|
return {
|
||
|
|
"success": True,
|
||
|
|
"skill": skill.summary(),
|
||
|
|
"content": skill.content,
|
||
|
|
"active_skills": active,
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
def _dispatch_task(ctx: ToolContext, args: Dict[str, Any]) -> Dict[str, Any]:
|
||
|
|
created: List[Dict[str, Any]] = []
|
||
|
|
for item in args["tasks"]:
|
||
|
|
task = ctx.dispatcher.create_task(
|
||
|
|
title=item["title"],
|
||
|
|
description=item["description"],
|
||
|
|
depends_on=item.get("depends_on") or [],
|
||
|
|
assignee=item.get("assignee"),
|
||
|
|
)
|
||
|
|
created.append(task.to_dict())
|
||
|
|
return {"success": True, "created_tasks": created}
|
||
|
|
|
||
|
|
|
||
|
|
def _list_tasks(ctx: ToolContext, args: Dict[str, Any]) -> Dict[str, Any]:
|
||
|
|
return {
|
||
|
|
"success": True,
|
||
|
|
"tasks": ctx.dispatcher.list_tasks(),
|
||
|
|
"next_ready_task": ctx.dispatcher.next_ready_task(),
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
def _update_task(ctx: ToolContext, args: Dict[str, Any]) -> Dict[str, Any]:
|
||
|
|
metadata = {}
|
||
|
|
if args.get("notes"):
|
||
|
|
metadata["notes"] = args["notes"]
|
||
|
|
task = ctx.dispatcher.update_task(
|
||
|
|
args["task_id"],
|
||
|
|
status=args.get("status"),
|
||
|
|
metadata=metadata,
|
||
|
|
)
|
||
|
|
return {"success": True, "task": task.to_dict()}
|
||
|
|
|
||
|
|
|
||
|
|
def _read_file(ctx: ToolContext, args: Dict[str, Any]) -> Dict[str, Any]:
|
||
|
|
path = _safe_workspace_path(ctx.workspace, args["path"])
|
||
|
|
return {"success": True, "path": str(path), "content": path.read_text(encoding="utf-8")}
|
||
|
|
|
||
|
|
|
||
|
|
def _execute_shell(ctx: ToolContext, args: Dict[str, Any]) -> Dict[str, Any]:
|
||
|
|
timeout = int(args.get("timeout_seconds", 20))
|
||
|
|
proc = subprocess.run(
|
||
|
|
_default_shell_command(args["command"]),
|
||
|
|
cwd=str(ctx.workspace),
|
||
|
|
capture_output=True,
|
||
|
|
text=True,
|
||
|
|
timeout=timeout,
|
||
|
|
shell=False,
|
||
|
|
)
|
||
|
|
return {
|
||
|
|
"success": proc.returncode == 0,
|
||
|
|
"returncode": proc.returncode,
|
||
|
|
"stdout": _trim_output(proc.stdout),
|
||
|
|
"stderr": _trim_output(proc.stderr),
|
||
|
|
"command": args["command"],
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
def _run_python(ctx: ToolContext, args: Dict[str, Any]) -> Dict[str, Any]:
|
||
|
|
timeout = int(args.get("timeout_seconds", 20))
|
||
|
|
proc = subprocess.run(
|
||
|
|
[sys.executable, "-c", args["code"]],
|
||
|
|
cwd=str(ctx.workspace),
|
||
|
|
capture_output=True,
|
||
|
|
text=True,
|
||
|
|
timeout=timeout,
|
||
|
|
shell=False,
|
||
|
|
)
|
||
|
|
return {
|
||
|
|
"success": proc.returncode == 0,
|
||
|
|
"returncode": proc.returncode,
|
||
|
|
"stdout": _trim_output(proc.stdout),
|
||
|
|
"stderr": _trim_output(proc.stderr),
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
def _write_file(ctx: ToolContext, args: Dict[str, Any]) -> Dict[str, Any]:
|
||
|
|
path = _safe_workspace_path(ctx.workspace, args["path"])
|
||
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||
|
|
path.write_text(args["content"], encoding="utf-8")
|
||
|
|
return {"success": True, "path": str(path), "bytes_written": len(args["content"].encode("utf-8"))}
|
||
|
|
|
||
|
|
|
||
|
|
def _safe_workspace_path(workspace: Path, value: str) -> Path:
|
||
|
|
candidate = (workspace / value).resolve()
|
||
|
|
workspace = workspace.resolve()
|
||
|
|
if candidate != workspace and workspace not in candidate.parents:
|
||
|
|
raise ValueError(f"Path escapes workspace: {value}")
|
||
|
|
return candidate
|
||
|
|
|
||
|
|
|
||
|
|
def _default_shell_command(command: str) -> List[str]:
|
||
|
|
if platform.system().lower().startswith("win"):
|
||
|
|
return ["powershell", "-Command", command]
|
||
|
|
return ["bash", "-lc", command]
|
||
|
|
|
||
|
|
|
||
|
|
def _trim_output(text: str, limit: int = 12000) -> str:
|
||
|
|
text = text or ""
|
||
|
|
if len(text) <= limit:
|
||
|
|
return text
|
||
|
|
return text[:limit] + "\n...[truncated]"
|