my_agent/tools/builtin.py

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]"