mita-code 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. mita/__init__.py +5 -0
  2. mita/__main__.py +5 -0
  3. mita/agent/__init__.py +1 -0
  4. mita/agent/context.py +43 -0
  5. mita/agent/conversation.py +101 -0
  6. mita/agent/loop.py +594 -0
  7. mita/agent/system_prompt.py +75 -0
  8. mita/cli.py +940 -0
  9. mita/config/__init__.py +6 -0
  10. mita/config/defaults.py +41 -0
  11. mita/config/loader.py +53 -0
  12. mita/config/schema.py +131 -0
  13. mita/hooks/__init__.py +1 -0
  14. mita/hooks/manager.py +94 -0
  15. mita/hooks/runner.py +145 -0
  16. mita/index/__init__.py +1 -0
  17. mita/index/embeddings.py +49 -0
  18. mita/index/manager.py +170 -0
  19. mita/index/parser.py +331 -0
  20. mita/index/retriever.py +53 -0
  21. mita/index/store.py +143 -0
  22. mita/llm/__init__.py +1 -0
  23. mita/llm/client.py +86 -0
  24. mita/llm/instructor.py +80 -0
  25. mita/llm/streaming.py +58 -0
  26. mita/memory/__init__.py +6 -0
  27. mita/memory/discovery.py +61 -0
  28. mita/memory/loader.py +76 -0
  29. mita/memory/manager.py +117 -0
  30. mita/models/__init__.py +13 -0
  31. mita/models/hardware.py +289 -0
  32. mita/models/manager.py +268 -0
  33. mita/models/ollama_client.py +104 -0
  34. mita/models/recommender.py +88 -0
  35. mita/models/registry.py +167 -0
  36. mita/models/server.py +262 -0
  37. mita/plugins/__init__.py +1 -0
  38. mita/plugins/client.py +152 -0
  39. mita/plugins/manager.py +210 -0
  40. mita/py.typed +0 -0
  41. mita/skills/__init__.py +1 -0
  42. mita/skills/executor.py +84 -0
  43. mita/skills/loader.py +117 -0
  44. mita/skills/manager.py +129 -0
  45. mita/tools/__init__.py +1 -0
  46. mita/tools/builtins/__init__.py +28 -0
  47. mita/tools/builtins/file_edit.py +71 -0
  48. mita/tools/builtins/file_read.py +74 -0
  49. mita/tools/builtins/file_write.py +42 -0
  50. mita/tools/builtins/git.py +112 -0
  51. mita/tools/builtins/glob_tool.py +67 -0
  52. mita/tools/builtins/grep_tool.py +93 -0
  53. mita/tools/builtins/shell.py +83 -0
  54. mita/tools/executor.py +80 -0
  55. mita/tools/registry.py +69 -0
  56. mita/tools/safety.py +91 -0
  57. mita/tools/schema.py +87 -0
  58. mita/ui/__init__.py +1 -0
  59. mita/ui/display.py +139 -0
  60. mita/ui/repl.py +88 -0
  61. mita/ui/spinner.py +48 -0
  62. mita/ui/theme.py +23 -0
  63. mita_code-0.1.0.dist-info/METADATA +227 -0
  64. mita_code-0.1.0.dist-info/RECORD +67 -0
  65. mita_code-0.1.0.dist-info/WHEEL +4 -0
  66. mita_code-0.1.0.dist-info/entry_points.txt +3 -0
  67. mita_code-0.1.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,74 @@
1
+ """Built-in tool: read file contents."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from mita.tools.schema import ToolDefinition, ToolParameter, ToolResult
9
+
10
+ TOOL_DEF = ToolDefinition(
11
+ name="file_read",
12
+ description="Read the contents of a file. Returns the file content with line numbers.",
13
+ parameters=[
14
+ ToolParameter(name="path", type="string", description="Path to the file to read."),
15
+ ToolParameter(
16
+ name="offset",
17
+ type="integer",
18
+ description="Line number to start reading from (1-based). Default: 1.",
19
+ required=False,
20
+ default=1,
21
+ ),
22
+ ToolParameter(
23
+ name="limit",
24
+ type="integer",
25
+ description="Maximum number of lines to read. Default: 2000.",
26
+ required=False,
27
+ default=2000,
28
+ ),
29
+ ],
30
+ destructive=False,
31
+ )
32
+
33
+
34
+ async def execute(args: dict[str, Any]) -> ToolResult:
35
+ """Read file contents with optional offset and limit."""
36
+ path_str = str(args.get("path", ""))
37
+ offset = int(args.get("offset", 1) or 1)
38
+ limit = int(args.get("limit", 2000) or 2000)
39
+
40
+ if not path_str:
41
+ return ToolResult(tool_call_id="", success=False, error="Missing required parameter: path")
42
+
43
+ path = Path(path_str).expanduser().resolve()
44
+
45
+ if not path.exists():
46
+ return ToolResult(tool_call_id="", success=False, error=f"File not found: {path}")
47
+
48
+ if not path.is_file():
49
+ return ToolResult(tool_call_id="", success=False, error=f"Not a file: {path}")
50
+
51
+ try:
52
+ text = path.read_text(encoding="utf-8", errors="replace")
53
+ except OSError as e:
54
+ return ToolResult(tool_call_id="", success=False, error=f"Cannot read file: {e}")
55
+
56
+ lines = text.splitlines()
57
+ total_lines = len(lines)
58
+
59
+ # Apply offset (1-based) and limit
60
+ start = max(0, offset - 1)
61
+ end = start + limit
62
+ selected = lines[start:end]
63
+
64
+ # Format with line numbers
65
+ numbered = []
66
+ for i, line in enumerate(selected, start=start + 1):
67
+ numbered.append(f"{i:>6}\t{line}")
68
+
69
+ output = "\n".join(numbered)
70
+
71
+ if end < total_lines:
72
+ output += f"\n\n... [{total_lines - end} more lines, {total_lines} total]"
73
+
74
+ return ToolResult(tool_call_id="", success=True, output=output)
@@ -0,0 +1,42 @@
1
+ """Built-in tool: write file contents."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from mita.tools.schema import ToolDefinition, ToolParameter, ToolResult
9
+
10
+ TOOL_DEF = ToolDefinition(
11
+ name="file_write",
12
+ description="Write content to a file. Creates the file and any parent directories if needed.",
13
+ parameters=[
14
+ ToolParameter(name="path", type="string", description="Path to the file to write."),
15
+ ToolParameter(name="content", type="string", description="Content to write to the file."),
16
+ ],
17
+ destructive=True, # Overwrites existing files
18
+ )
19
+
20
+
21
+ async def execute(args: dict[str, Any]) -> ToolResult:
22
+ """Write content to a file."""
23
+ path_str = str(args.get("path", ""))
24
+ content = str(args.get("content", ""))
25
+
26
+ if not path_str:
27
+ return ToolResult(tool_call_id="", success=False, error="Missing required parameter: path")
28
+
29
+ path = Path(path_str).expanduser().resolve()
30
+
31
+ try:
32
+ path.parent.mkdir(parents=True, exist_ok=True)
33
+ existed = path.exists()
34
+ path.write_text(content, encoding="utf-8")
35
+ action = "Updated" if existed else "Created"
36
+ return ToolResult(
37
+ tool_call_id="",
38
+ success=True,
39
+ output=f"{action} {path} ({len(content)} chars, {content.count(chr(10)) + 1} lines)",
40
+ )
41
+ except OSError as e:
42
+ return ToolResult(tool_call_id="", success=False, error=f"Cannot write file: {e}")
@@ -0,0 +1,112 @@
1
+ """Built-in tool: git operations (status, diff, commit, log)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import shlex
7
+ from typing import Any
8
+
9
+ from mita.tools.schema import ToolDefinition, ToolParameter, ToolResult
10
+
11
+ TOOL_DEF = ToolDefinition(
12
+ name="git",
13
+ description=(
14
+ "Run git commands. Supports: status, diff, log, add, commit, branch, checkout. "
15
+ "Destructive operations (push, reset, clean) require confirmation."
16
+ ),
17
+ parameters=[
18
+ ToolParameter(
19
+ name="subcommand",
20
+ type="string",
21
+ description=(
22
+ "The git subcommand to run "
23
+ '(e.g. "status", "diff", "log --oneline -10", "add .", "commit -m msg").'
24
+ ),
25
+ ),
26
+ ],
27
+ destructive=False, # Per-command destructiveness handled by is_safe_git_command
28
+ )
29
+
30
+ # Git subcommands that are always safe (read-only)
31
+ SAFE_SUBCOMMANDS = frozenset(
32
+ {
33
+ "status",
34
+ "diff",
35
+ "log",
36
+ "show",
37
+ "branch",
38
+ "remote",
39
+ "tag",
40
+ "stash",
41
+ "ls-files",
42
+ "rev-parse",
43
+ "describe",
44
+ }
45
+ )
46
+
47
+
48
+ def is_safe_git_command(subcommand: str) -> bool:
49
+ """Check if a git subcommand is read-only."""
50
+ first_word = subcommand.strip().split()[0] if subcommand.strip() else ""
51
+ return first_word in SAFE_SUBCOMMANDS
52
+
53
+
54
+ async def execute(args: dict[str, Any]) -> ToolResult:
55
+ """Execute a git subcommand using subprocess_exec (no shell injection)."""
56
+ subcommand = str(args.get("subcommand", ""))
57
+
58
+ if not subcommand:
59
+ return ToolResult(
60
+ tool_call_id="",
61
+ success=False,
62
+ error="Missing required parameter: subcommand",
63
+ )
64
+
65
+ # Use shlex.split to safely tokenize, then prepend "git"
66
+ try:
67
+ cmd_parts = ["git", *shlex.split(subcommand)]
68
+ except ValueError as e:
69
+ return ToolResult(
70
+ tool_call_id="",
71
+ success=False,
72
+ error=f"Invalid subcommand syntax: {e}",
73
+ )
74
+
75
+ try:
76
+ proc = await asyncio.create_subprocess_exec(
77
+ *cmd_parts,
78
+ stdout=asyncio.subprocess.PIPE,
79
+ stderr=asyncio.subprocess.PIPE,
80
+ )
81
+ stdout_bytes, stderr_bytes = await asyncio.wait_for(proc.communicate(), timeout=30)
82
+ except TimeoutError:
83
+ proc.kill()
84
+ return ToolResult(
85
+ tool_call_id="",
86
+ success=False,
87
+ error=f"Git command timed out: git {subcommand}",
88
+ )
89
+ except OSError as e:
90
+ return ToolResult(tool_call_id="", success=False, error=f"Failed to run git: {e}")
91
+
92
+ stdout = stdout_bytes.decode("utf-8", errors="replace") if stdout_bytes else ""
93
+ stderr = stderr_bytes.decode("utf-8", errors="replace") if stderr_bytes else ""
94
+
95
+ output_parts: list[str] = []
96
+ if stdout:
97
+ output_parts.append(stdout)
98
+ if stderr:
99
+ output_parts.append(f"STDERR:\n{stderr}")
100
+
101
+ output = "\n".join(output_parts) if output_parts else "(no output)"
102
+ exit_code = proc.returncode or 0
103
+
104
+ if exit_code != 0:
105
+ output = f"[exit code {exit_code}]\n{output}"
106
+
107
+ return ToolResult(
108
+ tool_call_id="",
109
+ success=exit_code == 0,
110
+ output=output,
111
+ error=f"Git command failed with exit code {exit_code}" if exit_code != 0 else None,
112
+ )
@@ -0,0 +1,67 @@
1
+ """Built-in tool: glob-based file search."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from mita.tools.schema import ToolDefinition, ToolParameter, ToolResult
9
+
10
+ TOOL_DEF = ToolDefinition(
11
+ name="glob",
12
+ description="Search for files matching a glob pattern. Returns matching file paths.",
13
+ parameters=[
14
+ ToolParameter(
15
+ name="pattern",
16
+ type="string",
17
+ description='Glob pattern (e.g. "**/*.py", "src/**/*.ts").',
18
+ ),
19
+ ToolParameter(
20
+ name="path",
21
+ type="string",
22
+ description="Directory to search in. Defaults to current directory.",
23
+ required=False,
24
+ default=".",
25
+ ),
26
+ ],
27
+ destructive=False,
28
+ )
29
+
30
+ MAX_RESULTS = 500
31
+
32
+
33
+ async def execute(args: dict[str, Any]) -> ToolResult:
34
+ """Search for files matching a glob pattern."""
35
+ pattern = str(args.get("pattern", ""))
36
+ base_path = str(args.get("path", ".") or ".")
37
+
38
+ if not pattern:
39
+ return ToolResult(
40
+ tool_call_id="", success=False, error="Missing required parameter: pattern"
41
+ )
42
+
43
+ base = Path(base_path).expanduser().resolve()
44
+ if not base.is_dir():
45
+ return ToolResult(tool_call_id="", success=False, error=f"Not a directory: {base}")
46
+
47
+ try:
48
+ matches = sorted(base.glob(pattern))
49
+ except ValueError as e:
50
+ return ToolResult(tool_call_id="", success=False, error=f"Invalid glob pattern: {e}")
51
+
52
+ # Filter out directories, keep only files
53
+ files = [str(m) for m in matches if m.is_file()]
54
+
55
+ if not files:
56
+ return ToolResult(tool_call_id="", success=True, output="No files matched.")
57
+
58
+ truncated = len(files) > MAX_RESULTS
59
+ output_files = files[:MAX_RESULTS]
60
+ output = "\n".join(output_files)
61
+
62
+ if truncated:
63
+ output += f"\n\n... [{len(files)} total matches, showing first {MAX_RESULTS}]"
64
+ else:
65
+ output += f"\n\n[{len(files)} file(s) matched]"
66
+
67
+ return ToolResult(tool_call_id="", success=True, output=output, truncated=truncated)
@@ -0,0 +1,93 @@
1
+ """Built-in tool: content search (ripgrep-style)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from mita.tools.schema import ToolDefinition, ToolParameter, ToolResult
10
+
11
+ TOOL_DEF = ToolDefinition(
12
+ name="grep",
13
+ description="Search file contents for a regex pattern. Returns matching lines with context.",
14
+ parameters=[
15
+ ToolParameter(
16
+ name="pattern",
17
+ type="string",
18
+ description="Regex pattern to search for.",
19
+ ),
20
+ ToolParameter(
21
+ name="path",
22
+ type="string",
23
+ description="File or directory to search in. Defaults to current directory.",
24
+ required=False,
25
+ default=".",
26
+ ),
27
+ ToolParameter(
28
+ name="include",
29
+ type="string",
30
+ description='Glob pattern to filter files (e.g. "*.py"). Only used for directories.',
31
+ required=False,
32
+ ),
33
+ ],
34
+ destructive=False,
35
+ )
36
+
37
+ MAX_MATCHES = 200
38
+
39
+
40
+ async def execute(args: dict[str, Any]) -> ToolResult:
41
+ """Search file contents for a regex pattern."""
42
+ pattern_str = str(args.get("pattern", ""))
43
+ path_str = str(args.get("path", ".") or ".")
44
+ include = args.get("include")
45
+
46
+ if not pattern_str:
47
+ return ToolResult(
48
+ tool_call_id="", success=False, error="Missing required parameter: pattern"
49
+ )
50
+
51
+ try:
52
+ regex = re.compile(pattern_str)
53
+ except re.error as e:
54
+ return ToolResult(tool_call_id="", success=False, error=f"Invalid regex: {e}")
55
+
56
+ target = Path(path_str).expanduser().resolve()
57
+
58
+ if target.is_file():
59
+ files = [target]
60
+ elif target.is_dir():
61
+ glob_pattern = str(include) if include else "**/*"
62
+ files = sorted(f for f in target.glob(glob_pattern) if f.is_file())
63
+ else:
64
+ return ToolResult(tool_call_id="", success=False, error=f"Path not found: {target}")
65
+
66
+ matches: list[str] = []
67
+ match_count = 0
68
+
69
+ for file_path in files:
70
+ try:
71
+ text = file_path.read_text(encoding="utf-8", errors="replace")
72
+ except OSError:
73
+ continue
74
+
75
+ for line_num, line in enumerate(text.splitlines(), 1):
76
+ if regex.search(line):
77
+ matches.append(f"{file_path}:{line_num}: {line.rstrip()}")
78
+ match_count += 1
79
+ if match_count >= MAX_MATCHES:
80
+ break
81
+ if match_count >= MAX_MATCHES:
82
+ break
83
+
84
+ if not matches:
85
+ return ToolResult(tool_call_id="", success=True, output="No matches found.")
86
+
87
+ output = "\n".join(matches)
88
+ if match_count >= MAX_MATCHES:
89
+ output += f"\n\n... [showing first {MAX_MATCHES} matches]"
90
+ else:
91
+ output += f"\n\n[{match_count} match(es)]"
92
+
93
+ return ToolResult(tool_call_id="", success=True, output=output)
@@ -0,0 +1,83 @@
1
+ """Built-in tool: shell command execution with timeout."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from typing import Any
7
+
8
+ from mita.tools.schema import ToolDefinition, ToolParameter, ToolResult
9
+
10
+ TOOL_DEF = ToolDefinition(
11
+ name="shell",
12
+ description=(
13
+ "Execute a shell command. Commands run in a bash subprocess with a timeout. "
14
+ "Use for system commands, builds, tests, etc."
15
+ ),
16
+ parameters=[
17
+ ToolParameter(
18
+ name="command",
19
+ type="string",
20
+ description="The shell command to execute.",
21
+ ),
22
+ ToolParameter(
23
+ name="timeout",
24
+ type="integer",
25
+ description="Timeout in seconds. Default: 120.",
26
+ required=False,
27
+ default=120,
28
+ ),
29
+ ],
30
+ destructive=True, # Shell commands can be destructive
31
+ )
32
+
33
+
34
+ async def execute(args: dict[str, Any]) -> ToolResult:
35
+ """Execute a shell command."""
36
+ command = str(args.get("command", ""))
37
+ timeout = int(args.get("timeout", 120) or 120)
38
+
39
+ if not command:
40
+ return ToolResult(
41
+ tool_call_id="", success=False, error="Missing required parameter: command"
42
+ )
43
+
44
+ try:
45
+ proc = await asyncio.create_subprocess_shell(
46
+ command,
47
+ stdout=asyncio.subprocess.PIPE,
48
+ stderr=asyncio.subprocess.PIPE,
49
+ )
50
+ stdout_bytes, stderr_bytes = await asyncio.wait_for(proc.communicate(), timeout=timeout)
51
+ except TimeoutError:
52
+ proc.kill()
53
+ return ToolResult(
54
+ tool_call_id="",
55
+ success=False,
56
+ error=f"Command timed out after {timeout}s: {command}",
57
+ )
58
+ except OSError as e:
59
+ return ToolResult(tool_call_id="", success=False, error=f"Failed to run command: {e}")
60
+
61
+ stdout = stdout_bytes.decode("utf-8", errors="replace") if stdout_bytes else ""
62
+ stderr = stderr_bytes.decode("utf-8", errors="replace") if stderr_bytes else ""
63
+
64
+ output_parts: list[str] = []
65
+ if stdout:
66
+ output_parts.append(stdout)
67
+ if stderr:
68
+ output_parts.append(f"STDERR:\n{stderr}")
69
+
70
+ output = (
71
+ "\n".join(output_parts) if output_parts else "Command completed successfully (no output)."
72
+ )
73
+ exit_code = proc.returncode or 0
74
+
75
+ if exit_code != 0:
76
+ output = f"[exit code {exit_code}]\n{output}"
77
+
78
+ return ToolResult(
79
+ tool_call_id="",
80
+ success=exit_code == 0,
81
+ output=output,
82
+ error=f"Command exited with code {exit_code}" if exit_code != 0 else None,
83
+ )
mita/tools/executor.py ADDED
@@ -0,0 +1,80 @@
1
+ """Tool dispatch: confirmation flow and execution."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from mita.config.schema import ToolSettings
6
+ from mita.tools.registry import ToolRegistry
7
+ from mita.tools.safety import is_command_banned, needs_confirmation
8
+ from mita.tools.schema import ToolCall, ToolResult
9
+
10
+
11
+ async def execute_tool(
12
+ tool_call: ToolCall,
13
+ registry: ToolRegistry,
14
+ settings: ToolSettings,
15
+ confirm_fn: object | None = None,
16
+ ) -> ToolResult:
17
+ """Execute a tool call with safety checks and optional confirmation.
18
+
19
+ Args:
20
+ tool_call: The parsed tool call from LLM output.
21
+ registry: The tool registry to look up handlers.
22
+ settings: Tool settings (auto_approve, banned_commands, etc.).
23
+ confirm_fn: Optional async callable(str) -> bool for user confirmation.
24
+ If None, destructive tools are denied automatically.
25
+ """
26
+ tool_def = registry.get_definition(tool_call.name)
27
+ if tool_def is None:
28
+ return ToolResult(
29
+ tool_call_id=tool_call.id,
30
+ success=False,
31
+ error=f"Unknown tool: {tool_call.name}",
32
+ )
33
+
34
+ # Check for banned commands (shell and git tools)
35
+ if tool_call.name == "shell":
36
+ command = tool_call.arguments.get("command", "")
37
+ if is_command_banned(command, settings.banned_commands):
38
+ return ToolResult(
39
+ tool_call_id=tool_call.id,
40
+ success=False,
41
+ error=f"Command is banned by configuration: {command}",
42
+ )
43
+ if tool_call.name == "git":
44
+ subcommand = tool_call.arguments.get("subcommand", "")
45
+ if is_command_banned(f"git {subcommand}", settings.banned_commands):
46
+ return ToolResult(
47
+ tool_call_id=tool_call.id,
48
+ success=False,
49
+ error=f"Git command is banned by configuration: git {subcommand}",
50
+ )
51
+
52
+ # Confirmation flow
53
+ if needs_confirmation(tool_call, tool_def, settings):
54
+ if confirm_fn is None:
55
+ return ToolResult(
56
+ tool_call_id=tool_call.id,
57
+ success=False,
58
+ error="Destructive action requires confirmation, but no confirmation handler.",
59
+ )
60
+
61
+ prompt = f"Allow {tool_call.name}({_summarize_args(tool_call)})?"
62
+ approved = await confirm_fn(prompt) # type: ignore[operator]
63
+ if not approved:
64
+ return ToolResult(
65
+ tool_call_id=tool_call.id,
66
+ success=False,
67
+ error="User denied this action.",
68
+ )
69
+
70
+ return await registry.execute(tool_call)
71
+
72
+
73
+ def _summarize_args(tool_call: ToolCall) -> str:
74
+ """Create a short summary of tool call arguments for the confirmation prompt."""
75
+ parts: list[str] = []
76
+ for key, value in tool_call.arguments.items():
77
+ if isinstance(value, str) and len(value) > 50:
78
+ value = value[:50] + "..."
79
+ parts.append(f"{key}={value!r}")
80
+ return ", ".join(parts)
mita/tools/registry.py ADDED
@@ -0,0 +1,69 @@
1
+ """Tool registry: name → (definition, handler) mapping."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Awaitable, Callable
6
+ from typing import Any
7
+
8
+ from mita.tools.schema import ToolCall, ToolDefinition, ToolResult
9
+
10
+ # Type for async tool handler functions
11
+ ToolHandler = Callable[[dict[str, Any]], Awaitable[ToolResult]]
12
+
13
+
14
+ class ToolRegistry:
15
+ """Central registry for all available tools (built-in + plugins)."""
16
+
17
+ def __init__(self) -> None:
18
+ self._tools: dict[str, ToolDefinition] = {}
19
+ self._handlers: dict[str, ToolHandler] = {}
20
+
21
+ def register(self, definition: ToolDefinition, handler: ToolHandler) -> None:
22
+ """Register a tool with its definition and handler."""
23
+ self._tools[definition.name] = definition
24
+ self._handlers[definition.name] = handler
25
+
26
+ def get_definition(self, name: str) -> ToolDefinition | None:
27
+ """Get a tool definition by name."""
28
+ return self._tools.get(name)
29
+
30
+ def get_definitions(self) -> list[ToolDefinition]:
31
+ """Get all registered tool definitions."""
32
+ return list(self._tools.values())
33
+
34
+ def get_openai_schemas(self) -> list[dict[str, Any]]:
35
+ """Get all tool definitions as OpenAI-compatible schemas."""
36
+ return [t.to_openai_schema() for t in self._tools.values()]
37
+
38
+ def has_tool(self, name: str) -> bool:
39
+ """Check if a tool is registered."""
40
+ return name in self._tools
41
+
42
+ async def execute(self, tool_call: ToolCall) -> ToolResult:
43
+ """Execute a tool call and return the result."""
44
+ handler = self._handlers.get(tool_call.name)
45
+ if handler is None:
46
+ return ToolResult(
47
+ tool_call_id=tool_call.id,
48
+ success=False,
49
+ error=f"Unknown tool: {tool_call.name}",
50
+ )
51
+ result = await handler(tool_call.arguments)
52
+ # Attach the tool_call_id
53
+ result = result.model_copy(update={"tool_call_id": tool_call.id})
54
+ return result.truncate_output()
55
+
56
+ @property
57
+ def tool_names(self) -> list[str]:
58
+ """List all registered tool names."""
59
+ return list(self._tools.keys())
60
+
61
+
62
+ def create_default_registry() -> ToolRegistry:
63
+ """Create a registry with all built-in tools registered."""
64
+ from mita.tools.builtins import BUILTIN_TOOLS
65
+
66
+ registry = ToolRegistry()
67
+ for _name, (definition, handler) in BUILTIN_TOOLS.items():
68
+ registry.register(definition, handler) # type: ignore[arg-type]
69
+ return registry