janito 2.2.0__py3-none-any.whl → 2.3.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 (130) hide show
  1. janito/__init__.py +6 -6
  2. janito/agent/setup_agent.py +14 -5
  3. janito/agent/templates/profiles/system_prompt_template_main.txt.j2 +3 -1
  4. janito/cli/chat_mode/bindings.py +6 -0
  5. janito/cli/chat_mode/session.py +16 -0
  6. janito/cli/chat_mode/shell/autocomplete.py +21 -21
  7. janito/cli/chat_mode/shell/commands/__init__.py +3 -0
  8. janito/cli/chat_mode/shell/commands/clear.py +12 -12
  9. janito/cli/chat_mode/shell/commands/exec.py +27 -0
  10. janito/cli/chat_mode/shell/commands/multi.py +51 -51
  11. janito/cli/chat_mode/shell/commands/tools.py +17 -6
  12. janito/cli/chat_mode/shell/input_history.py +62 -62
  13. janito/cli/chat_mode/shell/session/manager.py +1 -0
  14. janito/cli/chat_mode/toolbar.py +1 -0
  15. janito/cli/cli_commands/list_models.py +35 -35
  16. janito/cli/cli_commands/list_providers.py +9 -9
  17. janito/cli/cli_commands/list_tools.py +53 -53
  18. janito/cli/cli_commands/model_selection.py +50 -50
  19. janito/cli/cli_commands/model_utils.py +13 -2
  20. janito/cli/cli_commands/set_api_key.py +19 -19
  21. janito/cli/cli_commands/show_config.py +51 -51
  22. janito/cli/cli_commands/show_system_prompt.py +62 -62
  23. janito/cli/config.py +2 -1
  24. janito/cli/core/__init__.py +4 -4
  25. janito/cli/core/event_logger.py +59 -59
  26. janito/cli/core/getters.py +3 -1
  27. janito/cli/core/runner.py +165 -148
  28. janito/cli/core/setters.py +5 -1
  29. janito/cli/core/unsetters.py +54 -54
  30. janito/cli/main_cli.py +12 -1
  31. janito/cli/prompt_core.py +5 -2
  32. janito/cli/rich_terminal_reporter.py +22 -3
  33. janito/cli/single_shot_mode/__init__.py +6 -6
  34. janito/cli/single_shot_mode/handler.py +11 -1
  35. janito/cli/verbose_output.py +1 -1
  36. janito/config.py +5 -5
  37. janito/config_manager.py +2 -0
  38. janito/driver_events.py +14 -0
  39. janito/drivers/anthropic/driver.py +113 -113
  40. janito/drivers/azure_openai/driver.py +38 -3
  41. janito/drivers/driver_registry.py +0 -2
  42. janito/drivers/openai/driver.py +196 -36
  43. janito/formatting_token.py +54 -54
  44. janito/i18n/__init__.py +35 -35
  45. janito/i18n/messages.py +23 -23
  46. janito/i18n/pt.py +47 -47
  47. janito/llm/__init__.py +5 -5
  48. janito/llm/agent.py +443 -443
  49. janito/llm/auth.py +1 -0
  50. janito/llm/driver.py +7 -1
  51. janito/llm/driver_config.py +1 -0
  52. janito/llm/driver_config_builder.py +34 -34
  53. janito/llm/driver_input.py +12 -12
  54. janito/llm/message_parts.py +60 -60
  55. janito/llm/model.py +38 -38
  56. janito/llm/provider.py +196 -196
  57. janito/provider_config.py +7 -3
  58. janito/provider_registry.py +176 -158
  59. janito/providers/__init__.py +1 -0
  60. janito/providers/anthropic/model_info.py +22 -22
  61. janito/providers/anthropic/provider.py +2 -2
  62. janito/providers/azure_openai/model_info.py +7 -6
  63. janito/providers/azure_openai/provider.py +30 -2
  64. janito/providers/deepseek/__init__.py +1 -1
  65. janito/providers/deepseek/model_info.py +16 -16
  66. janito/providers/deepseek/provider.py +91 -91
  67. janito/providers/google/model_info.py +21 -29
  68. janito/providers/google/provider.py +49 -38
  69. janito/providers/mistralai/provider.py +2 -2
  70. janito/providers/provider_static_info.py +2 -3
  71. janito/tools/adapters/__init__.py +1 -1
  72. janito/tools/adapters/local/adapter.py +33 -11
  73. janito/tools/adapters/local/ask_user.py +102 -102
  74. janito/tools/adapters/local/copy_file.py +84 -84
  75. janito/tools/adapters/local/create_directory.py +69 -69
  76. janito/tools/adapters/local/create_file.py +82 -82
  77. janito/tools/adapters/local/delete_text_in_file.py +4 -7
  78. janito/tools/adapters/local/fetch_url.py +97 -97
  79. janito/tools/adapters/local/find_files.py +138 -138
  80. janito/tools/adapters/local/get_file_outline/__init__.py +1 -1
  81. janito/tools/adapters/local/get_file_outline/core.py +117 -117
  82. janito/tools/adapters/local/get_file_outline/java_outline.py +40 -40
  83. janito/tools/adapters/local/get_file_outline/markdown_outline.py +14 -14
  84. janito/tools/adapters/local/get_file_outline/python_outline.py +303 -303
  85. janito/tools/adapters/local/get_file_outline/python_outline_v2.py +156 -156
  86. janito/tools/adapters/local/get_file_outline/search_outline.py +33 -33
  87. janito/tools/adapters/local/move_file.py +3 -13
  88. janito/tools/adapters/local/python_code_run.py +166 -166
  89. janito/tools/adapters/local/python_command_run.py +164 -164
  90. janito/tools/adapters/local/python_file_run.py +163 -163
  91. janito/tools/adapters/local/remove_directory.py +6 -17
  92. janito/tools/adapters/local/remove_file.py +4 -10
  93. janito/tools/adapters/local/replace_text_in_file.py +6 -9
  94. janito/tools/adapters/local/run_bash_command.py +176 -176
  95. janito/tools/adapters/local/run_powershell_command.py +219 -219
  96. janito/tools/adapters/local/search_text/__init__.py +1 -1
  97. janito/tools/adapters/local/search_text/core.py +201 -201
  98. janito/tools/adapters/local/search_text/match_lines.py +1 -1
  99. janito/tools/adapters/local/search_text/pattern_utils.py +73 -73
  100. janito/tools/adapters/local/search_text/traverse_directory.py +145 -145
  101. janito/tools/adapters/local/validate_file_syntax/__init__.py +1 -1
  102. janito/tools/adapters/local/validate_file_syntax/core.py +106 -106
  103. janito/tools/adapters/local/validate_file_syntax/css_validator.py +35 -35
  104. janito/tools/adapters/local/validate_file_syntax/html_validator.py +93 -93
  105. janito/tools/adapters/local/validate_file_syntax/js_validator.py +27 -27
  106. janito/tools/adapters/local/validate_file_syntax/json_validator.py +6 -6
  107. janito/tools/adapters/local/validate_file_syntax/markdown_validator.py +109 -109
  108. janito/tools/adapters/local/validate_file_syntax/ps1_validator.py +32 -32
  109. janito/tools/adapters/local/validate_file_syntax/python_validator.py +5 -5
  110. janito/tools/adapters/local/validate_file_syntax/xml_validator.py +11 -11
  111. janito/tools/adapters/local/validate_file_syntax/yaml_validator.py +6 -6
  112. janito/tools/adapters/local/view_file.py +167 -167
  113. janito/tools/inspect_registry.py +17 -17
  114. janito/tools/tool_base.py +105 -105
  115. janito/tools/tool_events.py +58 -58
  116. janito/tools/tool_run_exception.py +12 -12
  117. janito/tools/tool_use_tracker.py +81 -81
  118. janito/tools/tool_utils.py +45 -45
  119. janito/tools/tools_adapter.py +78 -6
  120. janito/tools/tools_schema.py +104 -104
  121. janito/version.py +4 -4
  122. {janito-2.2.0.dist-info → janito-2.3.0.dist-info}/METADATA +388 -251
  123. janito-2.3.0.dist-info/RECORD +181 -0
  124. janito/drivers/google_genai/driver.py +0 -54
  125. janito/drivers/google_genai/schema_generator.py +0 -67
  126. janito-2.2.0.dist-info/RECORD +0 -182
  127. {janito-2.2.0.dist-info → janito-2.3.0.dist-info}/WHEEL +0 -0
  128. {janito-2.2.0.dist-info → janito-2.3.0.dist-info}/entry_points.txt +0 -0
  129. {janito-2.2.0.dist-info → janito-2.3.0.dist-info}/licenses/LICENSE +0 -0
  130. {janito-2.2.0.dist-info → janito-2.3.0.dist-info}/top_level.txt +0 -0
@@ -1,58 +1,58 @@
1
- import attr
2
- from typing import Any, ClassVar
3
- from janito.event_bus.event import Event
4
-
5
-
6
- @attr.s(auto_attribs=True, kw_only=True)
7
- class ToolEvent(Event):
8
- """
9
- Base class for events related to tool calls (external or internal tools).
10
- Includes tool name and request ID for correlation.
11
- """
12
-
13
- category: ClassVar[str] = "tool"
14
- tool_name: str
15
- request_id: str
16
-
17
-
18
- @attr.s(auto_attribs=True, kw_only=True)
19
- class ToolCallStarted(ToolEvent):
20
- """
21
- Event indicating that a tool call has started.
22
- Contains the arguments passed to the tool.
23
- """
24
-
25
- arguments: Any
26
-
27
-
28
- @attr.s(auto_attribs=True, kw_only=True)
29
- class ToolCallFinished(ToolEvent):
30
- """
31
- Event indicating that a tool call has finished.
32
- Contains the result returned by the tool.
33
- """
34
-
35
- result: Any
36
-
37
-
38
- @attr.s(auto_attribs=True, kw_only=True)
39
- class ToolRunError(ToolEvent):
40
- """
41
- Event indicating that an error occurred during tool execution (for event bus, not exception handling).
42
- """
43
-
44
- error: str
45
- exception: Exception = None
46
- arguments: Any = None
47
-
48
-
49
- @attr.s(auto_attribs=True, kw_only=True)
50
- class ToolCallError(ToolEvent):
51
- """
52
- Event indicating that the tool could not be called (e.g., tool not found, invalid arguments, or invocation failure).
53
- This is distinct from ToolRunError, which is for errors during execution after the tool has started running.
54
- """
55
-
56
- error: str
57
- exception: Exception = None
58
- arguments: Any = None
1
+ import attr
2
+ from typing import Any, ClassVar
3
+ from janito.event_bus.event import Event
4
+
5
+
6
+ @attr.s(auto_attribs=True, kw_only=True)
7
+ class ToolEvent(Event):
8
+ """
9
+ Base class for events related to tool calls (external or internal tools).
10
+ Includes tool name and request ID for correlation.
11
+ """
12
+
13
+ category: ClassVar[str] = "tool"
14
+ tool_name: str
15
+ request_id: str
16
+
17
+
18
+ @attr.s(auto_attribs=True, kw_only=True)
19
+ class ToolCallStarted(ToolEvent):
20
+ """
21
+ Event indicating that a tool call has started.
22
+ Contains the arguments passed to the tool.
23
+ """
24
+
25
+ arguments: Any
26
+
27
+
28
+ @attr.s(auto_attribs=True, kw_only=True)
29
+ class ToolCallFinished(ToolEvent):
30
+ """
31
+ Event indicating that a tool call has finished.
32
+ Contains the result returned by the tool.
33
+ """
34
+
35
+ result: Any
36
+
37
+
38
+ @attr.s(auto_attribs=True, kw_only=True)
39
+ class ToolRunError(ToolEvent):
40
+ """
41
+ Event indicating that an error occurred during tool execution (for event bus, not exception handling).
42
+ """
43
+
44
+ error: str
45
+ exception: Exception = None
46
+ arguments: Any = None
47
+
48
+
49
+ @attr.s(auto_attribs=True, kw_only=True)
50
+ class ToolCallError(ToolEvent):
51
+ """
52
+ Event indicating that the tool could not be called (e.g., tool not found, invalid arguments, or invocation failure).
53
+ This is distinct from ToolRunError, which is for errors during execution after the tool has started running.
54
+ """
55
+
56
+ error: str
57
+ exception: Exception = None
58
+ arguments: Any = None
@@ -1,12 +1,12 @@
1
- class ToolRunException(Exception):
2
- """
3
- Exception raised when a tool runs but fails due to an internal error or runtime exception.
4
- This is distinct from ToolRunError event, which is for event bus notification.
5
- """
6
-
7
- def __init__(self, tool_name, error, arguments=None, exception=None):
8
- self.tool_name = tool_name
9
- self.error = error
10
- self.arguments = arguments
11
- self.original_exception = exception
12
- super().__init__(f"ToolRunException: {tool_name}: {error}")
1
+ class ToolRunException(Exception):
2
+ """
3
+ Exception raised when a tool runs but fails due to an internal error or runtime exception.
4
+ This is distinct from ToolRunError event, which is for event bus notification.
5
+ """
6
+
7
+ def __init__(self, tool_name, error, arguments=None, exception=None):
8
+ self.tool_name = tool_name
9
+ self.error = error
10
+ self.arguments = arguments
11
+ self.original_exception = exception
12
+ super().__init__(f"ToolRunException: {tool_name}: {error}")
@@ -1,81 +1,81 @@
1
- import threading
2
- import os
3
- from typing import Any, Dict, List
4
-
5
-
6
- def normalize_path(path: str) -> str:
7
- if not isinstance(path, str):
8
- return path
9
- return os.path.normcase(os.path.abspath(path))
10
-
11
-
12
- class ToolUseTracker:
13
- _instance = None
14
- _lock = threading.Lock()
15
-
16
- def __new__(cls):
17
- if not cls._instance:
18
- with cls._lock:
19
- if not cls._instance:
20
- cls._instance = super().__new__(cls)
21
- cls._instance._history = []
22
- return cls._instance
23
-
24
- def record(self, tool_name: str, params: Dict[str, Any], result: Any = None):
25
- # Normalize file_path in params if present
26
- norm_params = params.copy()
27
- if "file_path" in norm_params:
28
- norm_params["file_path"] = normalize_path(norm_params["file_path"])
29
- self._history.append(
30
- {"tool": tool_name, "params": norm_params, "result": result}
31
- )
32
-
33
- def get_history(self) -> List[Dict[str, Any]]:
34
- return list(self._history)
35
-
36
- def get_operations_on_file(self, file_path: str) -> List[Dict[str, Any]]:
37
- norm_file_path = normalize_path(file_path)
38
- ops = []
39
- for entry in self._history:
40
- params = entry["params"]
41
- # Normalize any string param values for comparison
42
- for v in params.values():
43
- if isinstance(v, str) and normalize_path(v) == norm_file_path:
44
- ops.append(entry)
45
- break
46
- return ops
47
-
48
- def file_fully_read(self, file_path: str) -> bool:
49
- norm_file_path = normalize_path(file_path)
50
- for entry in self._history:
51
- if entry["tool"] == "view_file":
52
- params = entry["params"]
53
- if (
54
- "file_path" in params
55
- and normalize_path(params["file_path"]) == norm_file_path
56
- ):
57
- # If both from_line and to_line are None, full file was read
58
- if (
59
- params.get("from_line") is None
60
- and params.get("to_line") is None
61
- ):
62
- return True
63
- return False
64
-
65
- def last_operation_is_full_read_or_replace(self, file_path: str) -> bool:
66
- ops = self.get_operations_on_file(file_path)
67
- if not ops:
68
- return False
69
- last = ops[-1]
70
- if last["tool"] == "view_file":
71
- params = last["params"]
72
- if params.get("from_line") is None and params.get("to_line") is None:
73
- return True
74
- return False
75
-
76
- def clear_history(self):
77
- self._history.clear()
78
-
79
- @classmethod
80
- def instance(cls):
81
- return cls()
1
+ import threading
2
+ import os
3
+ from typing import Any, Dict, List
4
+
5
+
6
+ def normalize_path(path: str) -> str:
7
+ if not isinstance(path, str):
8
+ return path
9
+ return os.path.normcase(os.path.abspath(path))
10
+
11
+
12
+ class ToolUseTracker:
13
+ _instance = None
14
+ _lock = threading.Lock()
15
+
16
+ def __new__(cls):
17
+ if not cls._instance:
18
+ with cls._lock:
19
+ if not cls._instance:
20
+ cls._instance = super().__new__(cls)
21
+ cls._instance._history = []
22
+ return cls._instance
23
+
24
+ def record(self, tool_name: str, params: Dict[str, Any], result: Any = None):
25
+ # Normalize file_path in params if present
26
+ norm_params = params.copy()
27
+ if "file_path" in norm_params:
28
+ norm_params["file_path"] = normalize_path(norm_params["file_path"])
29
+ self._history.append(
30
+ {"tool": tool_name, "params": norm_params, "result": result}
31
+ )
32
+
33
+ def get_history(self) -> List[Dict[str, Any]]:
34
+ return list(self._history)
35
+
36
+ def get_operations_on_file(self, file_path: str) -> List[Dict[str, Any]]:
37
+ norm_file_path = normalize_path(file_path)
38
+ ops = []
39
+ for entry in self._history:
40
+ params = entry["params"]
41
+ # Normalize any string param values for comparison
42
+ for v in params.values():
43
+ if isinstance(v, str) and normalize_path(v) == norm_file_path:
44
+ ops.append(entry)
45
+ break
46
+ return ops
47
+
48
+ def file_fully_read(self, file_path: str) -> bool:
49
+ norm_file_path = normalize_path(file_path)
50
+ for entry in self._history:
51
+ if entry["tool"] == "view_file":
52
+ params = entry["params"]
53
+ if (
54
+ "file_path" in params
55
+ and normalize_path(params["file_path"]) == norm_file_path
56
+ ):
57
+ # If both from_line and to_line are None, full file was read
58
+ if (
59
+ params.get("from_line") is None
60
+ and params.get("to_line") is None
61
+ ):
62
+ return True
63
+ return False
64
+
65
+ def last_operation_is_full_read_or_replace(self, file_path: str) -> bool:
66
+ ops = self.get_operations_on_file(file_path)
67
+ if not ops:
68
+ return False
69
+ last = ops[-1]
70
+ if last["tool"] == "view_file":
71
+ params = last["params"]
72
+ if params.get("from_line") is None and params.get("to_line") is None:
73
+ return True
74
+ return False
75
+
76
+ def clear_history(self):
77
+ self._history.clear()
78
+
79
+ @classmethod
80
+ def instance(cls):
81
+ return cls()
@@ -1,45 +1,45 @@
1
- """
2
- Utility functions for the janito project.
3
- Add your shared helper functions here.
4
- """
5
-
6
- import os
7
- import urllib.parse
8
-
9
-
10
- def example_utility_function(x):
11
- """A simple example utility function."""
12
- return f"Processed: {x}"
13
-
14
-
15
- def display_path(path):
16
- """
17
- Returns a display-friendly path. Injects an ANSI hyperlink to a local web file viewer using a hardcoded port.
18
- Args:
19
- path (str): Path to display.
20
- Returns:
21
- str: Display path, as an ANSI hyperlink.
22
- """
23
- from janito.cli.config import get_termweb_port
24
-
25
- port = get_termweb_port()
26
- if os.path.isabs(path):
27
- cwd = os.path.abspath(os.getcwd())
28
- abs_path = os.path.abspath(path)
29
- # Check if the absolute path is within the current working directory
30
- if abs_path.startswith(cwd + os.sep):
31
- disp = os.path.relpath(abs_path, cwd)
32
- else:
33
- disp = path
34
- else:
35
- disp = os.path.relpath(path)
36
- url = f"http://localhost:{port}/?path={urllib.parse.quote(path)}"
37
- # Use Rich markup for hyperlinks
38
- return f"[link={url}]{disp}[/link]"
39
-
40
-
41
- def pluralize(word: str, count: int) -> str:
42
- """Return the pluralized form of word if count != 1, unless word already ends with 's'."""
43
- if count == 1 or word.endswith("s"):
44
- return word
45
- return word + "s"
1
+ """
2
+ Utility functions for the janito project.
3
+ Add your shared helper functions here.
4
+ """
5
+
6
+ import os
7
+ import urllib.parse
8
+
9
+
10
+ def example_utility_function(x):
11
+ """A simple example utility function."""
12
+ return f"Processed: {x}"
13
+
14
+
15
+ def display_path(path):
16
+ """
17
+ Returns a display-friendly path. Injects an ANSI hyperlink to a local web file viewer using a hardcoded port.
18
+ Args:
19
+ path (str): Path to display.
20
+ Returns:
21
+ str: Display path, as an ANSI hyperlink.
22
+ """
23
+ from janito.cli.config import get_termweb_port
24
+
25
+ port = get_termweb_port()
26
+ if os.path.isabs(path):
27
+ cwd = os.path.abspath(os.getcwd())
28
+ abs_path = os.path.abspath(path)
29
+ # Check if the absolute path is within the current working directory
30
+ if abs_path.startswith(cwd + os.sep):
31
+ disp = os.path.relpath(abs_path, cwd)
32
+ else:
33
+ disp = path
34
+ else:
35
+ disp = os.path.relpath(path)
36
+ url = f"http://localhost:{port}/?path={urllib.parse.quote(path)}"
37
+ # Use Rich markup for hyperlinks
38
+ return f"[link={url}]{disp}[/link]"
39
+
40
+
41
+ def pluralize(word: str, count: int) -> str:
42
+ """Return the pluralized form of word if count != 1, unless word already ends with 's'."""
43
+ if count == 1 or word.endswith("s"):
44
+ return word
45
+ return word + "s"
@@ -13,11 +13,11 @@ class ToolsAdapterBase:
13
13
  """
14
14
 
15
15
  def __init__(
16
- self, tools=None, event_bus=None, allowed_tools: Optional[list] = None
16
+ self, tools=None, event_bus=None, enabled_tools: Optional[list] = None
17
17
  ):
18
18
  self._tools = tools or []
19
19
  self._event_bus = event_bus # event bus can be set on all adapters
20
- self._allowed_tools = set(allowed_tools) if allowed_tools is not None else None
20
+ self._enabled_tools = set(enabled_tools) if enabled_tools is not None else None
21
21
  self.verbose_tools = False
22
22
 
23
23
  def set_verbose_tools(self, value: bool):
@@ -32,8 +32,10 @@ class ToolsAdapterBase:
32
32
  self._event_bus = bus
33
33
 
34
34
  def get_tools(self):
35
- """Return the list of tools managed by this provider."""
36
- return self._tools
35
+ """Return the list of enabled tools managed by this provider."""
36
+ if self._enabled_tools is None:
37
+ return self._tools
38
+ return [tool for tool in self._tools if getattr(tool, 'tool_name', None) in self._enabled_tools]
37
39
 
38
40
  def add_tool(self, tool):
39
41
  self._tools.append(tool)
@@ -84,12 +86,82 @@ class ToolsAdapterBase:
84
86
 
85
87
  return result
86
88
 
89
+ def _get_tool_callable(self, tool):
90
+ """Helper to retrieve the primary callable of a tool instance."""
91
+ if callable(tool):
92
+ return tool
93
+ if hasattr(tool, "execute") and callable(getattr(tool, "execute")):
94
+ return getattr(tool, "execute")
95
+ if hasattr(tool, "run") and callable(getattr(tool, "run")):
96
+ return getattr(tool, "run")
97
+ raise ValueError("Provided tool is not executable.")
98
+
99
+ def _validate_arguments_against_signature(self, func, arguments: dict):
100
+ """Validate provided arguments against a callable signature.
101
+
102
+ Returns an error string if validation fails, otherwise ``None``.
103
+ """
104
+ import inspect
105
+
106
+ if arguments is None:
107
+ arguments = {}
108
+ # Ensure the input is a dict to avoid breaking the inspect-based logic
109
+ if not isinstance(arguments, dict):
110
+ return "Tool arguments should be provided as an object / mapping"
111
+
112
+ sig = inspect.signature(func)
113
+ params = sig.parameters
114
+
115
+ # Check for unexpected arguments (unless **kwargs is accepted)
116
+ accepts_kwargs = any(
117
+ p.kind == inspect.Parameter.VAR_KEYWORD for p in params.values()
118
+ )
119
+ if not accepts_kwargs:
120
+ unexpected = [k for k in arguments.keys() if k not in params]
121
+ if unexpected:
122
+ return (
123
+ "Unexpected argument(s): " + ", ".join(sorted(unexpected))
124
+ )
125
+
126
+ # Check for missing required arguments (ignoring *args / **kwargs / self)
127
+ required_params = [
128
+ name
129
+ for name, p in params.items()
130
+ if p.kind in (
131
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
132
+ inspect.Parameter.KEYWORD_ONLY,
133
+ )
134
+ and p.default is inspect._empty
135
+ and name != "self"
136
+ ]
137
+ missing = [name for name in required_params if name not in arguments]
138
+ if missing:
139
+ return "Missing required argument(s): " + ", ".join(sorted(missing))
140
+
141
+ return None
142
+
87
143
  def execute_by_name(
88
144
  self, tool_name: str, *args, request_id=None, arguments=None, **kwargs
89
145
  ):
90
146
  self._check_tool_permissions(tool_name, request_id, arguments)
91
147
  tool = self.get_tool(tool_name)
92
148
  self._ensure_tool_exists(tool, tool_name, request_id, arguments)
149
+ func = self._get_tool_callable(tool)
150
+ # First, validate arguments against the callable signature to catch unexpected / missing params
151
+ sig_error = self._validate_arguments_against_signature(func, arguments)
152
+ if sig_error:
153
+ if self._event_bus:
154
+ self._event_bus.publish(
155
+ ToolCallError(
156
+ tool_name=tool_name,
157
+ request_id=request_id,
158
+ error=sig_error,
159
+ arguments=arguments,
160
+ )
161
+ )
162
+ return sig_error
163
+
164
+ # Optionally validate against JSON schema if available
93
165
  schema = getattr(tool, "schema", None)
94
166
  if schema and arguments is not None:
95
167
  validation_error = self._validate_arguments_against_schema(
@@ -159,8 +231,8 @@ class ToolsAdapterBase:
159
231
  )
160
232
 
161
233
  def _check_tool_permissions(self, tool_name, request_id, arguments):
162
- if self._allowed_tools is not None and tool_name not in self._allowed_tools:
163
- error_msg = f"Tool '{tool_name}' is not permitted by adapter allow-list."
234
+ if self._enabled_tools is not None and tool_name not in self._enabled_tools:
235
+ error_msg = f"Tool '{tool_name}' is not enabled in this adapter."
164
236
  if self._event_bus:
165
237
  self._event_bus.publish(
166
238
  ToolCallError(