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.
Files changed (126) hide show
  1. package/bin/bone.js +39 -0
  2. package/package.json +25 -39
  3. package/LICENSE +0 -21
  4. package/README.md +0 -201
  5. package/bin/npm-wrapper.js +0 -235
  6. package/bin/rg +0 -0
  7. package/bin/rg.exe +0 -0
  8. package/config.yaml.example +0 -144
  9. package/prompts/main/ask_questions.md +0 -31
  10. package/prompts/main/batch_independent_calls.md +0 -5
  11. package/prompts/main/casual_interactions.md +0 -11
  12. package/prompts/main/code_references.md +0 -8
  13. package/prompts/main/communication_style.md +0 -12
  14. package/prompts/main/context_reliability.md +0 -12
  15. package/prompts/main/conversational_tool_calling.md +0 -15
  16. package/prompts/main/dream.md +0 -50
  17. package/prompts/main/editing_pattern.md +0 -13
  18. package/prompts/main/error_handling.md +0 -6
  19. package/prompts/main/exploration_pattern.md +0 -21
  20. package/prompts/main/intro.md +0 -1
  21. package/prompts/main/obsidian.md +0 -16
  22. package/prompts/main/obsidian_project.md +0 -79
  23. package/prompts/main/professional_objectivity.md +0 -3
  24. package/prompts/main/skills.md +0 -3
  25. package/prompts/main/targeted_searching.md +0 -10
  26. package/prompts/main/task_lists_pattern.md +0 -8
  27. package/prompts/main/temp_folder.md +0 -9
  28. package/prompts/main/think_before_acting.md +0 -10
  29. package/prompts/main/tone_and_style.md +0 -4
  30. package/prompts/main/tool_preferences.md +0 -24
  31. package/prompts/main/trust_subagent_context.md +0 -21
  32. package/prompts/main/when_to_use_sub_agent.md +0 -7
  33. package/prompts/micro/ask_questions.md +0 -1
  34. package/prompts/micro/batch_independent_calls.md +0 -1
  35. package/prompts/micro/casual_interactions.md +0 -1
  36. package/prompts/micro/code_references.md +0 -1
  37. package/prompts/micro/communication_style.md +0 -1
  38. package/prompts/micro/context_reliability.md +0 -1
  39. package/prompts/micro/conversational_tool_calling.md +0 -1
  40. package/prompts/micro/editing_pattern.md +0 -1
  41. package/prompts/micro/error_handling.md +0 -1
  42. package/prompts/micro/exploration_pattern.md +0 -1
  43. package/prompts/micro/intro.md +0 -1
  44. package/prompts/micro/obsidian.md +0 -4
  45. package/prompts/micro/obsidian_project.md +0 -5
  46. package/prompts/micro/professional_objectivity.md +0 -1
  47. package/prompts/micro/skills.md +0 -1
  48. package/prompts/micro/targeted_searching.md +0 -1
  49. package/prompts/micro/task_lists_pattern.md +0 -1
  50. package/prompts/micro/temp_folder.md +0 -1
  51. package/prompts/micro/think_before_acting.md +0 -5
  52. package/prompts/micro/tone_and_style.md +0 -1
  53. package/prompts/micro/tool_preferences.md +0 -1
  54. package/prompts/micro/trust_subagent_context.md +0 -1
  55. package/prompts/micro/when_to_use_sub_agent.md +0 -1
  56. package/requirements.txt +0 -9
  57. package/src/__init__.py +0 -11
  58. package/src/core/__init__.py +0 -1
  59. package/src/core/agentic.py +0 -1085
  60. package/src/core/chat_manager.py +0 -1577
  61. package/src/core/config_manager.py +0 -260
  62. package/src/core/cron.py +0 -578
  63. package/src/core/cron_allowlist.py +0 -118
  64. package/src/core/memory.py +0 -145
  65. package/src/core/metadata.py +0 -75
  66. package/src/core/retry.py +0 -71
  67. package/src/core/skills.py +0 -463
  68. package/src/core/sub_agent.py +0 -376
  69. package/src/core/tool_approval.py +0 -220
  70. package/src/core/tool_feedback.py +0 -789
  71. package/src/exceptions.py +0 -79
  72. package/src/llm/__init__.py +0 -1
  73. package/src/llm/client.py +0 -176
  74. package/src/llm/codex_provider.py +0 -350
  75. package/src/llm/config.py +0 -536
  76. package/src/llm/prompts.py +0 -494
  77. package/src/llm/providers.py +0 -438
  78. package/src/llm/streaming.py +0 -163
  79. package/src/llm/token_tracker.py +0 -399
  80. package/src/tools/__init__.py +0 -151
  81. package/src/tools/constants.py +0 -59
  82. package/src/tools/create_file.py +0 -136
  83. package/src/tools/directory.py +0 -389
  84. package/src/tools/edit.py +0 -549
  85. package/src/tools/file_reader.py +0 -322
  86. package/src/tools/helpers/__init__.py +0 -99
  87. package/src/tools/helpers/base.py +0 -599
  88. package/src/tools/helpers/converters.py +0 -44
  89. package/src/tools/helpers/file_helpers.py +0 -189
  90. package/src/tools/helpers/formatters.py +0 -411
  91. package/src/tools/helpers/loader.py +0 -145
  92. package/src/tools/helpers/parallel_executor.py +0 -231
  93. package/src/tools/helpers/path_resolver.py +0 -283
  94. package/src/tools/helpers/plugin_manifest.py +0 -185
  95. package/src/tools/obsidian.py +0 -96
  96. package/src/tools/review_sub_agent.py +0 -190
  97. package/src/tools/rg_search.py +0 -477
  98. package/src/tools/search_plugins.py +0 -177
  99. package/src/tools/select_option.py +0 -600
  100. package/src/tools/shell.py +0 -302
  101. package/src/tools/sub_agent.py +0 -139
  102. package/src/tools/task_list.py +0 -269
  103. package/src/tools/web_search.py +0 -61
  104. package/src/ui/__init__.py +0 -1
  105. package/src/ui/banner.py +0 -87
  106. package/src/ui/commands.py +0 -3131
  107. package/src/ui/displays.py +0 -239
  108. package/src/ui/loader.py +0 -284
  109. package/src/ui/main.py +0 -643
  110. package/src/ui/prompt_utils.py +0 -113
  111. package/src/ui/setting_selector.py +0 -590
  112. package/src/ui/setup_wizard.py +0 -294
  113. package/src/ui/sub_agent_panel.py +0 -234
  114. package/src/ui/tool_confirmation.py +0 -226
  115. package/src/utils/__init__.py +0 -1
  116. package/src/utils/citation_parser.py +0 -199
  117. package/src/utils/editor.py +0 -207
  118. package/src/utils/gitignore_filter.py +0 -149
  119. package/src/utils/logger.py +0 -254
  120. package/src/utils/paths.py +0 -30
  121. package/src/utils/result_parsers.py +0 -108
  122. package/src/utils/safe_commands.py +0 -243
  123. package/src/utils/settings.py +0 -195
  124. package/src/utils/user_message_logger.py +0 -120
  125. package/src/utils/validation.py +0 -201
  126. 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