bone-agent 1.4.0 → 2.0.0
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.
- package/bin/bone.js +39 -0
- package/package.json +25 -39
- package/LICENSE +0 -21
- package/README.md +0 -201
- package/bin/npm-wrapper.js +0 -235
- package/bin/rg +0 -0
- package/bin/rg.exe +0 -0
- package/config.yaml.example +0 -144
- package/prompts/main/ask_questions.md +0 -31
- package/prompts/main/batch_independent_calls.md +0 -5
- package/prompts/main/casual_interactions.md +0 -11
- package/prompts/main/code_references.md +0 -8
- package/prompts/main/communication_style.md +0 -12
- package/prompts/main/context_reliability.md +0 -12
- package/prompts/main/conversational_tool_calling.md +0 -15
- package/prompts/main/dream.md +0 -50
- package/prompts/main/editing_pattern.md +0 -13
- package/prompts/main/error_handling.md +0 -6
- package/prompts/main/exploration_pattern.md +0 -21
- package/prompts/main/intro.md +0 -1
- package/prompts/main/obsidian.md +0 -16
- package/prompts/main/obsidian_project.md +0 -79
- package/prompts/main/professional_objectivity.md +0 -3
- package/prompts/main/skills.md +0 -3
- package/prompts/main/targeted_searching.md +0 -10
- package/prompts/main/task_lists_pattern.md +0 -8
- package/prompts/main/temp_folder.md +0 -9
- package/prompts/main/think_before_acting.md +0 -10
- package/prompts/main/tone_and_style.md +0 -4
- package/prompts/main/tool_preferences.md +0 -24
- package/prompts/main/trust_subagent_context.md +0 -21
- package/prompts/main/when_to_use_sub_agent.md +0 -7
- package/prompts/micro/ask_questions.md +0 -1
- package/prompts/micro/batch_independent_calls.md +0 -1
- package/prompts/micro/casual_interactions.md +0 -1
- package/prompts/micro/code_references.md +0 -1
- package/prompts/micro/communication_style.md +0 -1
- package/prompts/micro/context_reliability.md +0 -1
- package/prompts/micro/conversational_tool_calling.md +0 -1
- package/prompts/micro/editing_pattern.md +0 -1
- package/prompts/micro/error_handling.md +0 -1
- package/prompts/micro/exploration_pattern.md +0 -1
- package/prompts/micro/intro.md +0 -1
- package/prompts/micro/obsidian.md +0 -4
- package/prompts/micro/obsidian_project.md +0 -5
- package/prompts/micro/professional_objectivity.md +0 -1
- package/prompts/micro/skills.md +0 -1
- package/prompts/micro/targeted_searching.md +0 -1
- package/prompts/micro/task_lists_pattern.md +0 -1
- package/prompts/micro/temp_folder.md +0 -1
- package/prompts/micro/think_before_acting.md +0 -5
- package/prompts/micro/tone_and_style.md +0 -1
- package/prompts/micro/tool_preferences.md +0 -1
- package/prompts/micro/trust_subagent_context.md +0 -1
- package/prompts/micro/when_to_use_sub_agent.md +0 -1
- package/requirements.txt +0 -9
- package/src/__init__.py +0 -11
- package/src/core/__init__.py +0 -1
- package/src/core/agentic.py +0 -1085
- package/src/core/chat_manager.py +0 -1577
- package/src/core/config_manager.py +0 -260
- package/src/core/cron.py +0 -578
- package/src/core/cron_allowlist.py +0 -118
- package/src/core/memory.py +0 -145
- package/src/core/metadata.py +0 -75
- package/src/core/retry.py +0 -71
- package/src/core/skills.py +0 -463
- package/src/core/sub_agent.py +0 -376
- package/src/core/tool_approval.py +0 -220
- package/src/core/tool_feedback.py +0 -789
- package/src/exceptions.py +0 -79
- package/src/llm/__init__.py +0 -1
- package/src/llm/client.py +0 -176
- package/src/llm/codex_provider.py +0 -350
- package/src/llm/config.py +0 -536
- package/src/llm/prompts.py +0 -494
- package/src/llm/providers.py +0 -438
- package/src/llm/streaming.py +0 -163
- package/src/llm/token_tracker.py +0 -399
- package/src/tools/__init__.py +0 -151
- package/src/tools/constants.py +0 -59
- package/src/tools/create_file.py +0 -136
- package/src/tools/directory.py +0 -389
- package/src/tools/edit.py +0 -549
- package/src/tools/file_reader.py +0 -322
- package/src/tools/helpers/__init__.py +0 -99
- package/src/tools/helpers/base.py +0 -599
- package/src/tools/helpers/converters.py +0 -44
- package/src/tools/helpers/file_helpers.py +0 -189
- package/src/tools/helpers/formatters.py +0 -411
- package/src/tools/helpers/loader.py +0 -145
- package/src/tools/helpers/parallel_executor.py +0 -231
- package/src/tools/helpers/path_resolver.py +0 -283
- package/src/tools/helpers/plugin_manifest.py +0 -185
- package/src/tools/obsidian.py +0 -96
- package/src/tools/review_sub_agent.py +0 -190
- package/src/tools/rg_search.py +0 -477
- package/src/tools/search_plugins.py +0 -177
- package/src/tools/select_option.py +0 -600
- package/src/tools/shell.py +0 -302
- package/src/tools/sub_agent.py +0 -139
- package/src/tools/task_list.py +0 -269
- package/src/tools/web_search.py +0 -61
- package/src/ui/__init__.py +0 -1
- package/src/ui/banner.py +0 -87
- package/src/ui/commands.py +0 -3131
- package/src/ui/displays.py +0 -239
- package/src/ui/loader.py +0 -284
- package/src/ui/main.py +0 -643
- package/src/ui/prompt_utils.py +0 -113
- package/src/ui/setting_selector.py +0 -590
- package/src/ui/setup_wizard.py +0 -294
- package/src/ui/sub_agent_panel.py +0 -234
- package/src/ui/tool_confirmation.py +0 -226
- package/src/utils/__init__.py +0 -1
- package/src/utils/citation_parser.py +0 -199
- package/src/utils/editor.py +0 -207
- package/src/utils/gitignore_filter.py +0 -149
- package/src/utils/logger.py +0 -254
- package/src/utils/paths.py +0 -30
- package/src/utils/result_parsers.py +0 -108
- package/src/utils/safe_commands.py +0 -243
- package/src/utils/settings.py +0 -195
- package/src/utils/user_message_logger.py +0 -120
- package/src/utils/validation.py +0 -201
- package/src/utils/web_search.py +0 -173
|
@@ -1,599 +0,0 @@
|
|
|
1
|
-
"""Tool registry and decorator for automatic tool registration.
|
|
2
|
-
|
|
3
|
-
This module provides the core infrastructure for defining tools with a
|
|
4
|
-
decorator-based pattern. Tools are automatically registered and can be
|
|
5
|
-
filtered by interaction mode.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
import logging
|
|
9
|
-
import inspect
|
|
10
|
-
from typing import Dict, List, Optional, Callable, Any
|
|
11
|
-
from dataclasses import dataclass, field
|
|
12
|
-
from functools import wraps
|
|
13
|
-
from pathlib import Path
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
_logger = logging.getLogger(__name__)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
# Terminal policy constants for thinking indicator handoff
|
|
20
|
-
TERMINAL_NONE = "none" # Indicator keeps running (non-interactive tools)
|
|
21
|
-
TERMINAL_YIELD = "yield" # Indicator pauses, clears line, tool takes over terminal (Live/prompt_toolkit)
|
|
22
|
-
TERMINAL_STOP = "stop" # Indicator fully stops (approval prompts need clean terminal)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
@dataclass
|
|
26
|
-
class ToolDefinition:
|
|
27
|
-
"""Definition of a tool including metadata and execution handler.
|
|
28
|
-
|
|
29
|
-
Attributes:
|
|
30
|
-
name: Tool identifier (e.g., "read_file")
|
|
31
|
-
description: Human-readable description of what the tool does
|
|
32
|
-
parameters: JSON Schema for tool parameters
|
|
33
|
-
requires_approval: Whether this tool requires user confirmation
|
|
34
|
-
terminal_policy: How this tool interacts with the thinking indicator
|
|
35
|
-
handler: Function that executes the tool
|
|
36
|
-
tier: "core" (always in context) or "plugin" (on-demand via search_plugins)
|
|
37
|
-
tags: List of searchable tags for plugin discovery
|
|
38
|
-
category: Category grouping for plugin discovery (e.g., "email", "database")
|
|
39
|
-
"""
|
|
40
|
-
name: str
|
|
41
|
-
description: str
|
|
42
|
-
parameters: Dict[str, Any]
|
|
43
|
-
requires_approval: bool = False
|
|
44
|
-
terminal_policy: str = TERMINAL_NONE # Default: indicator keeps running
|
|
45
|
-
handler: Optional[Callable] = None
|
|
46
|
-
tier: str = "core"
|
|
47
|
-
tags: List[str] = field(default_factory=list)
|
|
48
|
-
category: str = ""
|
|
49
|
-
|
|
50
|
-
def to_openai_schema(self) -> Dict[str, Any]:
|
|
51
|
-
"""Convert tool definition to OpenAI function-calling schema.
|
|
52
|
-
|
|
53
|
-
Returns:
|
|
54
|
-
Dictionary in OpenAI function format
|
|
55
|
-
"""
|
|
56
|
-
return {
|
|
57
|
-
"type": "function",
|
|
58
|
-
"function": {
|
|
59
|
-
"name": self.name,
|
|
60
|
-
"description": self.description,
|
|
61
|
-
"parameters": self.parameters
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
def execute(self, arguments: Dict[str, Any], context: Dict[str, Any]) -> str:
|
|
66
|
-
"""Execute the tool with given arguments and context.
|
|
67
|
-
|
|
68
|
-
Args:
|
|
69
|
-
arguments: Tool arguments from LLM
|
|
70
|
-
context: Execution context (repo_root, console, etc.)
|
|
71
|
-
|
|
72
|
-
Returns:
|
|
73
|
-
Tool result string with exit_code=N prefix
|
|
74
|
-
|
|
75
|
-
Raises:
|
|
76
|
-
RuntimeError: If no handler is registered
|
|
77
|
-
"""
|
|
78
|
-
if self.handler is None:
|
|
79
|
-
raise RuntimeError(f"Tool '{self.name}' has no registered handler")
|
|
80
|
-
|
|
81
|
-
# Get the handler's parameter names
|
|
82
|
-
sig = inspect.signature(self.handler)
|
|
83
|
-
handler_params = set(sig.parameters.keys())
|
|
84
|
-
|
|
85
|
-
# Inject context parameters only if the handler expects them
|
|
86
|
-
for key, value in context.items():
|
|
87
|
-
if key not in arguments and key in handler_params:
|
|
88
|
-
arguments[key] = value
|
|
89
|
-
|
|
90
|
-
return self.handler(**arguments)
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
# Named groups of related tools for bulk enable/disable
|
|
94
|
-
TOOL_GROUPS = {
|
|
95
|
-
"core": {
|
|
96
|
-
"label": "Core",
|
|
97
|
-
"tools": [
|
|
98
|
-
"rg", "read_file", "create_file", "edit_file", "list_directory",
|
|
99
|
-
"execute_command", "web_search", "sub_agent", "search_plugins",
|
|
100
|
-
"select_option", "create_task_list", "complete_task", "show_task_list",
|
|
101
|
-
],
|
|
102
|
-
},
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
class ToolRegistry:
|
|
107
|
-
"""Global registry for all tools.
|
|
108
|
-
|
|
109
|
-
Singleton pattern ensures all tools are registered in one place.
|
|
110
|
-
"""
|
|
111
|
-
|
|
112
|
-
_instance: Optional['ToolRegistry'] = None
|
|
113
|
-
_tools: Dict[str, ToolDefinition] = {}
|
|
114
|
-
_disabled: Dict[str, None] = {} # dict used as ordered set — mirrors _tools pattern
|
|
115
|
-
_plugin_ttl: Dict[str, int] = {} # tracks remaining turns for activated plugins
|
|
116
|
-
_default_plugin_ttl: int = 10
|
|
117
|
-
|
|
118
|
-
def __new__(cls):
|
|
119
|
-
if cls._instance is None:
|
|
120
|
-
cls._instance = super().__new__(cls)
|
|
121
|
-
return cls._instance
|
|
122
|
-
|
|
123
|
-
@classmethod
|
|
124
|
-
def _is_known_name(cls, name: str) -> bool:
|
|
125
|
-
"""Check if a name belongs to a registered tool or known plugin."""
|
|
126
|
-
if name in cls._tools:
|
|
127
|
-
return True
|
|
128
|
-
# Lazy check against plugin manifest for names not yet activated
|
|
129
|
-
from tools.helpers.plugin_manifest import plugin_manifest
|
|
130
|
-
return plugin_manifest.has_plugin(name)
|
|
131
|
-
|
|
132
|
-
@classmethod
|
|
133
|
-
def disable(cls, name: str) -> bool:
|
|
134
|
-
"""Disable a tool or plugin by name.
|
|
135
|
-
|
|
136
|
-
Works for core tools (in _tools) and plugins (known via manifest
|
|
137
|
-
but not yet activated).
|
|
138
|
-
|
|
139
|
-
Args:
|
|
140
|
-
name: Tool or plugin name to disable
|
|
141
|
-
|
|
142
|
-
Returns:
|
|
143
|
-
True if name was recognized and disabled
|
|
144
|
-
"""
|
|
145
|
-
if name in cls._tools or cls._is_known_name(name):
|
|
146
|
-
cls._disabled[name] = None
|
|
147
|
-
return True
|
|
148
|
-
return False
|
|
149
|
-
|
|
150
|
-
@classmethod
|
|
151
|
-
def enable(cls, name: str) -> bool:
|
|
152
|
-
"""Enable a previously disabled tool or plugin.
|
|
153
|
-
|
|
154
|
-
Args:
|
|
155
|
-
name: Tool or plugin name to enable
|
|
156
|
-
|
|
157
|
-
Returns:
|
|
158
|
-
True if it was disabled and is now re-enabled
|
|
159
|
-
"""
|
|
160
|
-
if name in cls._disabled:
|
|
161
|
-
del cls._disabled[name]
|
|
162
|
-
return True
|
|
163
|
-
return False
|
|
164
|
-
|
|
165
|
-
@classmethod
|
|
166
|
-
def is_disabled(cls, name: str) -> bool:
|
|
167
|
-
"""Check if a tool is currently disabled.
|
|
168
|
-
|
|
169
|
-
Args:
|
|
170
|
-
name: Tool name
|
|
171
|
-
|
|
172
|
-
Returns:
|
|
173
|
-
True if tool is disabled
|
|
174
|
-
"""
|
|
175
|
-
return name in cls._disabled
|
|
176
|
-
|
|
177
|
-
@classmethod
|
|
178
|
-
def disable_group(cls, group_key: str) -> list:
|
|
179
|
-
"""Disable all tools in a named group.
|
|
180
|
-
|
|
181
|
-
Args:
|
|
182
|
-
group_key: Key from TOOL_GROUPS (e.g. "file_ops")
|
|
183
|
-
|
|
184
|
-
Returns:
|
|
185
|
-
List of tool names that were actually disabled
|
|
186
|
-
"""
|
|
187
|
-
group = TOOL_GROUPS.get(group_key)
|
|
188
|
-
if not group:
|
|
189
|
-
return []
|
|
190
|
-
disabled = []
|
|
191
|
-
for name in group["tools"]:
|
|
192
|
-
if name in cls._tools and name not in cls._disabled:
|
|
193
|
-
cls._disabled[name] = None
|
|
194
|
-
disabled.append(name)
|
|
195
|
-
return disabled
|
|
196
|
-
|
|
197
|
-
@classmethod
|
|
198
|
-
def enable_group(cls, group_key: str) -> list:
|
|
199
|
-
"""Enable all tools in a named group.
|
|
200
|
-
|
|
201
|
-
Args:
|
|
202
|
-
group_key: Key from TOOL_GROUPS (e.g. "file_ops")
|
|
203
|
-
|
|
204
|
-
Returns:
|
|
205
|
-
List of tool names that were actually re-enabled
|
|
206
|
-
"""
|
|
207
|
-
group = TOOL_GROUPS.get(group_key)
|
|
208
|
-
if not group:
|
|
209
|
-
return []
|
|
210
|
-
enabled = []
|
|
211
|
-
for name in group["tools"]:
|
|
212
|
-
if name in cls._disabled:
|
|
213
|
-
del cls._disabled[name]
|
|
214
|
-
enabled.append(name)
|
|
215
|
-
return enabled
|
|
216
|
-
|
|
217
|
-
@classmethod
|
|
218
|
-
def get_group_status(cls, group_key: str) -> dict:
|
|
219
|
-
"""Get enabled/disabled status for all tools in a group.
|
|
220
|
-
|
|
221
|
-
Args:
|
|
222
|
-
group_key: Key from TOOL_GROUPS
|
|
223
|
-
|
|
224
|
-
Returns:
|
|
225
|
-
Dict with 'label', 'tools' list of {name, enabled} dicts
|
|
226
|
-
"""
|
|
227
|
-
group = TOOL_GROUPS.get(group_key)
|
|
228
|
-
if not group:
|
|
229
|
-
return {"label": group_key, "tools": []}
|
|
230
|
-
return {
|
|
231
|
-
"label": group["label"],
|
|
232
|
-
"tools": [
|
|
233
|
-
{"name": name, "enabled": name not in cls._disabled}
|
|
234
|
-
for name in group["tools"]
|
|
235
|
-
],
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
@classmethod
|
|
239
|
-
def get_disabled(cls) -> set:
|
|
240
|
-
"""Get the set of disabled tool names.
|
|
241
|
-
|
|
242
|
-
Returns:
|
|
243
|
-
Set of disabled tool names
|
|
244
|
-
"""
|
|
245
|
-
return set(cls._disabled)
|
|
246
|
-
|
|
247
|
-
@classmethod
|
|
248
|
-
def register(cls, tool_def: ToolDefinition) -> None:
|
|
249
|
-
"""Register a tool definition.
|
|
250
|
-
|
|
251
|
-
Args:
|
|
252
|
-
tool_def: ToolDefinition to register
|
|
253
|
-
|
|
254
|
-
Note:
|
|
255
|
-
Overwrites existing tools with same name (logs warning)
|
|
256
|
-
"""
|
|
257
|
-
if tool_def.name in cls._tools:
|
|
258
|
-
import warnings
|
|
259
|
-
warnings.warn(
|
|
260
|
-
f"Tool '{tool_def.name}' is being overwritten. "
|
|
261
|
-
f"Previous tool: {cls._tools[tool_def.name].handler}, "
|
|
262
|
-
f"New tool: {tool_def.handler}"
|
|
263
|
-
)
|
|
264
|
-
cls._tools[tool_def.name] = tool_def
|
|
265
|
-
|
|
266
|
-
@classmethod
|
|
267
|
-
def get(cls, name: str) -> Optional[ToolDefinition]:
|
|
268
|
-
"""Get a tool definition by name.
|
|
269
|
-
|
|
270
|
-
Args:
|
|
271
|
-
name: Tool name
|
|
272
|
-
|
|
273
|
-
Returns:
|
|
274
|
-
ToolDefinition or None if not found
|
|
275
|
-
"""
|
|
276
|
-
return cls._tools.get(name)
|
|
277
|
-
|
|
278
|
-
@classmethod
|
|
279
|
-
def get_all(cls, include_plugins: bool = False) -> List[ToolDefinition]:
|
|
280
|
-
"""Get all registered and enabled tools.
|
|
281
|
-
|
|
282
|
-
Args:
|
|
283
|
-
include_plugins: If True, include plugin-tier tools. Default: core only.
|
|
284
|
-
|
|
285
|
-
Returns:
|
|
286
|
-
List of all ToolDefinitions (excluding disabled, excluding plugins unless requested)
|
|
287
|
-
"""
|
|
288
|
-
tools = [t for t in cls._tools.values() if t.name not in cls._disabled]
|
|
289
|
-
if not include_plugins:
|
|
290
|
-
tools = [t for t in tools if t.tier != "plugin"]
|
|
291
|
-
return tools
|
|
292
|
-
|
|
293
|
-
@classmethod
|
|
294
|
-
def unregister(cls, name: str) -> bool:
|
|
295
|
-
"""Remove a tool from the registry by name.
|
|
296
|
-
|
|
297
|
-
Args:
|
|
298
|
-
name: Tool name to remove
|
|
299
|
-
|
|
300
|
-
Returns:
|
|
301
|
-
True if tool was found and removed, False if not registered
|
|
302
|
-
"""
|
|
303
|
-
cls._disabled.pop(name, None)
|
|
304
|
-
return cls._tools.pop(name, None) is not None
|
|
305
|
-
|
|
306
|
-
@classmethod
|
|
307
|
-
def clear(cls) -> None:
|
|
308
|
-
"""Clear all registered tools (mainly for testing)."""
|
|
309
|
-
cls._tools.clear()
|
|
310
|
-
cls._disabled.clear()
|
|
311
|
-
cls._plugin_ttl.clear()
|
|
312
|
-
|
|
313
|
-
@classmethod
|
|
314
|
-
def tool_count(cls) -> int:
|
|
315
|
-
"""Get the number of active (enabled) tools in the registry.
|
|
316
|
-
|
|
317
|
-
Returns:
|
|
318
|
-
Number of enabled tools (excludes disabled tools)
|
|
319
|
-
"""
|
|
320
|
-
return sum(1 for name in cls._tools if name not in cls._disabled)
|
|
321
|
-
|
|
322
|
-
# =========================================================================
|
|
323
|
-
# Plugin activation and TTL management
|
|
324
|
-
# =========================================================================
|
|
325
|
-
|
|
326
|
-
@classmethod
|
|
327
|
-
def activate_plugin(cls, tool_def, ttl=None) -> bool:
|
|
328
|
-
"""Activate a plugin-tier tool by registering it with a TTL.
|
|
329
|
-
|
|
330
|
-
Args:
|
|
331
|
-
tool_def: ToolDefinition with tier="plugin"
|
|
332
|
-
ttl: Number of turns before eviction (default: cls._default_plugin_ttl)
|
|
333
|
-
|
|
334
|
-
Returns:
|
|
335
|
-
True if the plugin was activated, False if it is disabled
|
|
336
|
-
"""
|
|
337
|
-
if cls.is_disabled(tool_def.name):
|
|
338
|
-
_logger.debug("Skipping activation for disabled plugin: %s", tool_def.name)
|
|
339
|
-
return False
|
|
340
|
-
if ttl is None:
|
|
341
|
-
ttl = cls._default_plugin_ttl
|
|
342
|
-
cls._tools[tool_def.name] = tool_def
|
|
343
|
-
cls._plugin_ttl[tool_def.name] = ttl
|
|
344
|
-
_logger.debug(f"Plugin activated: {tool_def.name} (TTL={ttl})")
|
|
345
|
-
return True
|
|
346
|
-
|
|
347
|
-
@classmethod
|
|
348
|
-
def deactivate_plugin(cls, name: str) -> bool:
|
|
349
|
-
"""Deactivate and remove a plugin-tier tool.
|
|
350
|
-
|
|
351
|
-
Args:
|
|
352
|
-
name: Plugin tool name to deactivate
|
|
353
|
-
|
|
354
|
-
Returns:
|
|
355
|
-
True if plugin was found and removed
|
|
356
|
-
"""
|
|
357
|
-
if name in cls._plugin_ttl:
|
|
358
|
-
del cls._plugin_ttl[name]
|
|
359
|
-
return cls._tools.pop(name, None) is not None
|
|
360
|
-
|
|
361
|
-
@classmethod
|
|
362
|
-
def decrement_plugin_ttls(cls):
|
|
363
|
-
"""Decrement TTL for all activated plugins. Evict those at zero.
|
|
364
|
-
|
|
365
|
-
Returns:
|
|
366
|
-
List of evicted plugin names
|
|
367
|
-
"""
|
|
368
|
-
evicted = []
|
|
369
|
-
expired = [name for name, ttl in cls._plugin_ttl.items() if ttl <= 1]
|
|
370
|
-
for name in expired:
|
|
371
|
-
cls.deactivate_plugin(name)
|
|
372
|
-
evicted.append(name)
|
|
373
|
-
_logger.debug(f"Plugin evicted (TTL expired): {name}")
|
|
374
|
-
# Decrement remaining
|
|
375
|
-
for name in cls._plugin_ttl:
|
|
376
|
-
cls._plugin_ttl[name] -= 1
|
|
377
|
-
return evicted
|
|
378
|
-
|
|
379
|
-
@classmethod
|
|
380
|
-
def touch_plugin(cls, name: str) -> None:
|
|
381
|
-
"""Reset TTL for an activated plugin (called when plugin is used).
|
|
382
|
-
|
|
383
|
-
Args:
|
|
384
|
-
name: Plugin tool name
|
|
385
|
-
"""
|
|
386
|
-
if name in cls._plugin_ttl:
|
|
387
|
-
cls._plugin_ttl[name] = cls._default_plugin_ttl
|
|
388
|
-
_logger.debug(f"Plugin TTL reset: {name}")
|
|
389
|
-
|
|
390
|
-
@classmethod
|
|
391
|
-
def active_plugin_names(cls) -> set:
|
|
392
|
-
"""Get the set of currently activated plugin names.
|
|
393
|
-
|
|
394
|
-
Returns:
|
|
395
|
-
Set of active plugin tool names
|
|
396
|
-
"""
|
|
397
|
-
return set(cls._plugin_ttl.keys())
|
|
398
|
-
|
|
399
|
-
@classmethod
|
|
400
|
-
def is_plugin_active(cls, name: str) -> bool:
|
|
401
|
-
"""Check if a plugin is currently activated in the registry.
|
|
402
|
-
|
|
403
|
-
Args:
|
|
404
|
-
name: Tool name
|
|
405
|
-
|
|
406
|
-
Returns:
|
|
407
|
-
True if tool is an active plugin
|
|
408
|
-
"""
|
|
409
|
-
return name in cls._plugin_ttl
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
def get_terminal_policy(tool_name: str) -> str:
|
|
413
|
-
"""Get the terminal policy for a tool.
|
|
414
|
-
|
|
415
|
-
Args:
|
|
416
|
-
tool_name: Name of the tool
|
|
417
|
-
|
|
418
|
-
Returns:
|
|
419
|
-
Terminal policy string (TERMINAL_NONE, TERMINAL_YIELD, or TERMINAL_STOP)
|
|
420
|
-
"""
|
|
421
|
-
tool_def = ToolRegistry.get(tool_name)
|
|
422
|
-
if tool_def:
|
|
423
|
-
return tool_def.terminal_policy
|
|
424
|
-
return TERMINAL_NONE # Default to none for unknown tools
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
def _enrich_plugin_metadata(tool_def: ToolDefinition) -> None:
|
|
428
|
-
"""Auto-generate description and/or tags for a plugin if missing."""
|
|
429
|
-
if tool_def.description and tool_def.tags:
|
|
430
|
-
return
|
|
431
|
-
|
|
432
|
-
try:
|
|
433
|
-
source = inspect.getsource(tool_def.handler) if tool_def.handler else ""
|
|
434
|
-
except (OSError, TypeError):
|
|
435
|
-
source = ""
|
|
436
|
-
|
|
437
|
-
content = f"{tool_def.name}\n{tool_def.description}\n{source}"
|
|
438
|
-
try:
|
|
439
|
-
from core.metadata import generate_metadata
|
|
440
|
-
generated = generate_metadata(content, tool_def.name)
|
|
441
|
-
except Exception:
|
|
442
|
-
_logger.debug("Plugin metadata enrichment failed for '%s'", tool_def.name, exc_info=True)
|
|
443
|
-
return
|
|
444
|
-
|
|
445
|
-
if not tool_def.description:
|
|
446
|
-
tool_def.description = str(generated.get("description", "")).strip()
|
|
447
|
-
if not tool_def.tags:
|
|
448
|
-
raw_tags = generated.get("tags")
|
|
449
|
-
if isinstance(raw_tags, str):
|
|
450
|
-
raw_tags = [raw_tags]
|
|
451
|
-
elif not isinstance(raw_tags, list):
|
|
452
|
-
raw_tags = []
|
|
453
|
-
tool_def.tags = [str(tag).strip() for tag in raw_tags if str(tag).strip()]
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
def tool(
|
|
457
|
-
name: str,
|
|
458
|
-
description: str,
|
|
459
|
-
parameters: Dict[str, Any],
|
|
460
|
-
requires_approval: bool = False,
|
|
461
|
-
terminal_policy: str = TERMINAL_NONE,
|
|
462
|
-
tier: str = "core",
|
|
463
|
-
tags: Optional[List[str]] = None,
|
|
464
|
-
category: str = ""
|
|
465
|
-
) -> Callable:
|
|
466
|
-
"""Decorator for registering tool functions.
|
|
467
|
-
|
|
468
|
-
Usage:
|
|
469
|
-
@tool(
|
|
470
|
-
name="my_tool",
|
|
471
|
-
description="Does something useful",
|
|
472
|
-
parameters={
|
|
473
|
-
"type": "object",
|
|
474
|
-
"properties": {
|
|
475
|
-
"input": {"type": "string", "description": "Input value"}
|
|
476
|
-
},
|
|
477
|
-
"required": ["input"]
|
|
478
|
-
},
|
|
479
|
-
)
|
|
480
|
-
def my_tool(input: str, repo_root: Path):
|
|
481
|
-
return f"exit_code=0\nProcessed: {input}"
|
|
482
|
-
|
|
483
|
-
Args:
|
|
484
|
-
name: Tool identifier
|
|
485
|
-
description: Human-readable description
|
|
486
|
-
parameters: JSON Schema for parameters
|
|
487
|
-
requires_approval: Whether confirmation is required (default: False)
|
|
488
|
-
terminal_policy: Terminal policy for thinking indicator (default: TERMINAL_NONE)
|
|
489
|
-
tier: "core" or "plugin" (default: "core")
|
|
490
|
-
tags: List of searchable tags for plugin discovery
|
|
491
|
-
category: Category grouping for plugin discovery
|
|
492
|
-
|
|
493
|
-
Returns:
|
|
494
|
-
Decorator function
|
|
495
|
-
|
|
496
|
-
Note:
|
|
497
|
-
The decorated function should return a string with exit_code=N prefix,
|
|
498
|
-
e.g., "exit_code=0\nResult content here"
|
|
499
|
-
|
|
500
|
-
Plugin-tier tools (tier="plugin") are registered in the PluginManifest
|
|
501
|
-
instead of ToolRegistry, so they don't consume context tokens by default.
|
|
502
|
-
They are activated on-demand via the search_plugins core tool.
|
|
503
|
-
"""
|
|
504
|
-
def decorator(func: Callable) -> Callable:
|
|
505
|
-
# Validate tier
|
|
506
|
-
if tier not in ("core", "plugin"):
|
|
507
|
-
raise ValueError(
|
|
508
|
-
f"Invalid tier '{tier}' for tool '{name}'. Must be 'core' or 'plugin'."
|
|
509
|
-
)
|
|
510
|
-
|
|
511
|
-
# Create tool definition
|
|
512
|
-
tool_def = ToolDefinition(
|
|
513
|
-
name=name,
|
|
514
|
-
description=description,
|
|
515
|
-
parameters=parameters,
|
|
516
|
-
requires_approval=requires_approval,
|
|
517
|
-
terminal_policy=terminal_policy,
|
|
518
|
-
handler=func,
|
|
519
|
-
tier=tier,
|
|
520
|
-
tags=tags or [],
|
|
521
|
-
category=category,
|
|
522
|
-
)
|
|
523
|
-
|
|
524
|
-
# Plugin-tier tools go to the manifest, not the registry
|
|
525
|
-
if tier == "plugin":
|
|
526
|
-
_enrich_plugin_metadata(tool_def)
|
|
527
|
-
from .plugin_manifest import plugin_manifest
|
|
528
|
-
plugin_manifest.register(tool_def)
|
|
529
|
-
else:
|
|
530
|
-
# Register core tool normally
|
|
531
|
-
ToolRegistry.register(tool_def)
|
|
532
|
-
|
|
533
|
-
@wraps(func)
|
|
534
|
-
def wrapper(*args, **kwargs):
|
|
535
|
-
return func(*args, **kwargs)
|
|
536
|
-
|
|
537
|
-
return wrapper
|
|
538
|
-
|
|
539
|
-
return decorator
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
def build_context(
|
|
543
|
-
repo_root: Path,
|
|
544
|
-
console: Any = None,
|
|
545
|
-
gitignore_spec: Any = None,
|
|
546
|
-
debug_mode: bool = False,
|
|
547
|
-
chat_manager: Any = None,
|
|
548
|
-
rg_exe_path: str = None,
|
|
549
|
-
panel_updater: Any = None,
|
|
550
|
-
vault_root: str = None
|
|
551
|
-
) -> Dict[str, Any]:
|
|
552
|
-
"""Build execution context for tool invocation.
|
|
553
|
-
|
|
554
|
-
Args:
|
|
555
|
-
repo_root: Repository root directory
|
|
556
|
-
console: Rich console for output
|
|
557
|
-
gitignore_spec: PathSpec for .gitignore filtering
|
|
558
|
-
debug_mode: Whether debug mode is enabled
|
|
559
|
-
chat_manager: ChatManager instance
|
|
560
|
-
rg_exe_path: Path to rg executable
|
|
561
|
-
panel_updater: Optional SubAgentPanel for live updates
|
|
562
|
-
vault_root: Optional Obsidian vault root path
|
|
563
|
-
|
|
564
|
-
Returns:
|
|
565
|
-
Context dictionary
|
|
566
|
-
"""
|
|
567
|
-
context = {
|
|
568
|
-
"repo_root": repo_root,
|
|
569
|
-
"console": console,
|
|
570
|
-
"gitignore_spec": gitignore_spec,
|
|
571
|
-
"debug_mode": debug_mode
|
|
572
|
-
}
|
|
573
|
-
if chat_manager is not None:
|
|
574
|
-
context["chat_manager"] = chat_manager
|
|
575
|
-
if rg_exe_path is not None:
|
|
576
|
-
context["rg_exe_path"] = rg_exe_path
|
|
577
|
-
if panel_updater is not None:
|
|
578
|
-
context["panel_updater"] = panel_updater
|
|
579
|
-
if vault_root is not None:
|
|
580
|
-
context["vault_root"] = vault_root
|
|
581
|
-
return context
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
# =============================================================================
|
|
585
|
-
# Tool schema exports for OpenAI function calling
|
|
586
|
-
# =============================================================================
|
|
587
|
-
|
|
588
|
-
def get_tool_schemas() -> list:
|
|
589
|
-
"""Generate OpenAI tool schemas from registry.
|
|
590
|
-
|
|
591
|
-
Returns:
|
|
592
|
-
List of tool schemas in OpenAI function-calling format
|
|
593
|
-
"""
|
|
594
|
-
return [tool.to_openai_schema() for tool in ToolRegistry.get_all(include_plugins=True)]
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
def TOOLS():
|
|
598
|
-
"""Get tool schemas. Callable for backward compatibility."""
|
|
599
|
-
return get_tool_schemas()
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
"""Shared type conversion utilities for tool argument validation."""
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
def coerce_int(value):
|
|
5
|
-
"""Best-effort coercion of tool arguments to int.
|
|
6
|
-
|
|
7
|
-
Returns:
|
|
8
|
-
Tuple of (int_value, error_message). error_message is None on success.
|
|
9
|
-
"""
|
|
10
|
-
if value is None:
|
|
11
|
-
return None, "Missing required integer value."
|
|
12
|
-
if isinstance(value, bool):
|
|
13
|
-
return None, "Value must be an integer, not a boolean."
|
|
14
|
-
if isinstance(value, int):
|
|
15
|
-
return value, None
|
|
16
|
-
if isinstance(value, str):
|
|
17
|
-
text = value.strip()
|
|
18
|
-
if text == "":
|
|
19
|
-
return None, "Value must be a non-empty integer."
|
|
20
|
-
try:
|
|
21
|
-
return int(text), None
|
|
22
|
-
except ValueError:
|
|
23
|
-
return None, "Value must be an integer."
|
|
24
|
-
return None, "Value must be an integer."
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def coerce_bool(value, default=None):
|
|
28
|
-
"""Best-effort coercion of tool arguments to boolean.
|
|
29
|
-
|
|
30
|
-
Returns None if value is None and default is None.
|
|
31
|
-
"""
|
|
32
|
-
if value is None:
|
|
33
|
-
return default
|
|
34
|
-
if isinstance(value, bool):
|
|
35
|
-
return value
|
|
36
|
-
if isinstance(value, int):
|
|
37
|
-
return bool(value)
|
|
38
|
-
if isinstance(value, str):
|
|
39
|
-
normalized = value.strip().lower()
|
|
40
|
-
if normalized in {"true", "1", "yes", "y", "on"}:
|
|
41
|
-
return True
|
|
42
|
-
if normalized in {"false", "0", "no", "n", "off"}:
|
|
43
|
-
return False
|
|
44
|
-
return default
|