dulus 0.2.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.
- agent.py +363 -0
- backend/__init__.py +63 -0
- backend/compressor.py +261 -0
- backend/context.py +329 -0
- backend/githook.py +166 -0
- backend/marketplace.py +141 -0
- backend/mempalace_bridge.py +182 -0
- backend/personas.py +297 -0
- backend/plugins.py +222 -0
- backend/server.py +411 -0
- backend/tasks.py +213 -0
- batch_api.py +307 -0
- checkpoint/__init__.py +27 -0
- checkpoint/hooks.py +90 -0
- checkpoint/store.py +314 -0
- checkpoint/types.py +80 -0
- claude_code_watcher.py +214 -0
- clipboard_utils.py +246 -0
- cloudsave.py +159 -0
- common.py +177 -0
- compaction.py +378 -0
- config.py +180 -0
- context.py +241 -0
- dulus-0.2.0.dist-info/METADATA +600 -0
- dulus-0.2.0.dist-info/RECORD +101 -0
- dulus-0.2.0.dist-info/WHEEL +5 -0
- dulus-0.2.0.dist-info/entry_points.txt +2 -0
- dulus-0.2.0.dist-info/licenses/LICENSE +674 -0
- dulus-0.2.0.dist-info/licenses/license_manager.py +187 -0
- dulus-0.2.0.dist-info/top_level.txt +36 -0
- dulus.py +8455 -0
- dulus_gui.py +331 -0
- dulus_mcp/__init__.py +43 -0
- dulus_mcp/client.py +546 -0
- dulus_mcp/config.py +133 -0
- dulus_mcp/tools.py +131 -0
- dulus_mcp/types.py +124 -0
- gui/__init__.py +18 -0
- gui/agent_bridge.py +283 -0
- gui/chat_widget.py +448 -0
- gui/main_window.py +485 -0
- gui/personas.py +230 -0
- gui/session_utils.py +189 -0
- gui/settings_dialog.py +146 -0
- gui/sidebar.py +515 -0
- gui/tasks_view.py +499 -0
- gui/themes.py +256 -0
- gui/tool_panel.py +94 -0
- input.py +1030 -0
- license_manager.py +187 -0
- memory/__init__.py +93 -0
- memory/audit.py +51 -0
- memory/consolidator.py +312 -0
- memory/context.py +270 -0
- memory/offload.py +148 -0
- memory/palace.py +127 -0
- memory/scan.py +146 -0
- memory/sessions.py +100 -0
- memory/store.py +395 -0
- memory/tools.py +408 -0
- memory/types.py +114 -0
- memory/vector_search.py +92 -0
- multi_agent/__init__.py +23 -0
- multi_agent/subagent.py +501 -0
- multi_agent/tools.py +393 -0
- offload_helper.py +183 -0
- plugin/__init__.py +22 -0
- plugin/autoadapter.py +1641 -0
- plugin/loader.py +156 -0
- plugin/recommend.py +211 -0
- plugin/store.py +387 -0
- plugin/types.py +147 -0
- providers.py +3750 -0
- skill/__init__.py +14 -0
- skill/builtin.py +100 -0
- skill/clawhub.py +270 -0
- skill/executor.py +66 -0
- skill/loader.py +199 -0
- skill/tools.py +110 -0
- skills.py +14 -0
- spinner.py +42 -0
- string_utils.py +42 -0
- subagent.py +11 -0
- task/__init__.py +12 -0
- task/store.py +199 -0
- task/tools.py +265 -0
- task/types.py +92 -0
- tmux_offloader.py +177 -0
- tmux_tools.py +410 -0
- tool_registry.py +214 -0
- tools.py +2694 -0
- ui/__init__.py +1 -0
- ui/input.py +464 -0
- ui/render.py +272 -0
- voice/__init__.py +56 -0
- voice/keyterms.py +179 -0
- voice/recorder.py +263 -0
- voice/stt.py +408 -0
- voice/tts.py +570 -0
- webchat.py +432 -0
- webchat_server.py +1761 -0
tool_registry.py
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"""Tool plugin registry for dulus.
|
|
2
|
+
|
|
3
|
+
Provides a central registry for tool definitions, lookup, schema export,
|
|
4
|
+
and dispatch with output truncation.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class ToolDef:
|
|
16
|
+
"""Definition of a single tool plugin.
|
|
17
|
+
|
|
18
|
+
Attributes:
|
|
19
|
+
name: unique tool identifier
|
|
20
|
+
schema: JSON-schema dict sent to the API (name, description, input_schema)
|
|
21
|
+
func: callable(params: dict, config: dict) -> str
|
|
22
|
+
read_only: True if the tool never mutates state
|
|
23
|
+
concurrent_safe: True if safe to run in parallel with other tools
|
|
24
|
+
display_only: True if output is visual/display only and should NOT be read back
|
|
25
|
+
(saves tokens - use for ASCII art, charts, visual output)
|
|
26
|
+
"""
|
|
27
|
+
name: str
|
|
28
|
+
schema: Dict[str, Any]
|
|
29
|
+
func: Callable[[Dict[str, Any], Dict[str, Any]], str]
|
|
30
|
+
read_only: bool = False
|
|
31
|
+
concurrent_safe: bool = False
|
|
32
|
+
display_only: bool = False # NEW: visual output, don't read back
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# --------------- internal state ---------------
|
|
36
|
+
|
|
37
|
+
_registry: Dict[str, ToolDef] = {}
|
|
38
|
+
_last_seen_turn: int = -1
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# --------------- public API ---------------
|
|
42
|
+
|
|
43
|
+
def register_tool(tool_def: ToolDef) -> None:
|
|
44
|
+
"""Register a tool, overwriting any existing tool with the same name."""
|
|
45
|
+
_registry[tool_def.name] = tool_def
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def get_tool(name: str) -> Optional[ToolDef]:
|
|
49
|
+
"""Look up a tool by name. Returns None if not found."""
|
|
50
|
+
return _registry.get(name)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def get_all_tools() -> List[ToolDef]:
|
|
54
|
+
"""Return all registered tools (insertion order)."""
|
|
55
|
+
return list(_registry.values())
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def get_tool_schemas() -> List[Dict[str, Any]]:
|
|
59
|
+
"""Return the schemas of all registered tools (for API tool parameter)."""
|
|
60
|
+
return [t.schema for t in _registry.values()]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def is_display_only(name: str) -> bool:
|
|
64
|
+
"""Check if a tool is display-only (visual output, don't read back).
|
|
65
|
+
|
|
66
|
+
Returns True if the tool's output should not be fed back to the model,
|
|
67
|
+
typically for ASCII art, visual charts, or display-only content.
|
|
68
|
+
"""
|
|
69
|
+
tool = get_tool(name)
|
|
70
|
+
return tool.display_only if tool else False
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def execute_tool(
|
|
74
|
+
name: str,
|
|
75
|
+
params: Dict[str, Any],
|
|
76
|
+
config: Dict[str, Any],
|
|
77
|
+
max_output: int = 2500,
|
|
78
|
+
) -> str:
|
|
79
|
+
"""Dispatch a tool call by name.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
name: tool name
|
|
83
|
+
params: tool input parameters dict
|
|
84
|
+
config: runtime configuration dict
|
|
85
|
+
max_output: maximum allowed output length in characters.
|
|
86
|
+
Default 2500 ā applies uniformly to built-ins AND plugins.
|
|
87
|
+
Tools that need more MUST paginate explicitly (Read with offset/limit, etc).
|
|
88
|
+
Callers can override via config["max_tool_output"] (see tools.execute_tool).
|
|
89
|
+
This prevents context bloat from 0.5% ā 7% on large outputs.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
Tool result string, possibly truncated with navigation hints.
|
|
93
|
+
"""
|
|
94
|
+
tool = get_tool(name)
|
|
95
|
+
if tool is None:
|
|
96
|
+
return f"Error: tool '{name}' not found."
|
|
97
|
+
|
|
98
|
+
import io
|
|
99
|
+
from contextlib import redirect_stdout, redirect_stderr
|
|
100
|
+
|
|
101
|
+
f_stdout = io.StringIO()
|
|
102
|
+
f_stderr = io.StringIO()
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
with redirect_stdout(f_stdout), redirect_stderr(f_stderr):
|
|
106
|
+
result = tool.func(params, config)
|
|
107
|
+
except Exception as e:
|
|
108
|
+
out = f_stdout.getvalue()
|
|
109
|
+
err = f_stderr.getvalue()
|
|
110
|
+
msg = f"Error executing {name}: {e}"
|
|
111
|
+
if out: msg += f"\nSTDOUT:\n{out}"
|
|
112
|
+
if err: msg += f"\nSTDERR:\n{err}"
|
|
113
|
+
|
|
114
|
+
# Add a heuristic hint if a plugin tool crashes
|
|
115
|
+
_mod = getattr(tool.func, "__module__", "")
|
|
116
|
+
if _mod.startswith("_plugin_"):
|
|
117
|
+
parts = _mod.split("_")
|
|
118
|
+
p_name = parts[2] if len(parts) > 2 else "unknown"
|
|
119
|
+
msg += f"\n\nš” Hint: This plugin tool failed. Do not guess the fix. Use Read/Bash to view ~/.dulus/plugins/{p_name}/plugin_tool.py and its documentation to understand the correct API usage."
|
|
120
|
+
|
|
121
|
+
return msg
|
|
122
|
+
|
|
123
|
+
out = f_stdout.getvalue()
|
|
124
|
+
err = f_stderr.getvalue()
|
|
125
|
+
|
|
126
|
+
if result is None:
|
|
127
|
+
result = ""
|
|
128
|
+
|
|
129
|
+
if not isinstance(result, str):
|
|
130
|
+
result = json.dumps(result, ensure_ascii=False, default=str)
|
|
131
|
+
|
|
132
|
+
# Merge captured output with return value
|
|
133
|
+
final_parts = []
|
|
134
|
+
if out.strip():
|
|
135
|
+
final_parts.append(out.strip())
|
|
136
|
+
if err.strip():
|
|
137
|
+
final_parts.append(f"--- STDERR ---\n{err.strip()}")
|
|
138
|
+
|
|
139
|
+
r_strip = result.strip()
|
|
140
|
+
if r_strip:
|
|
141
|
+
if final_parts:
|
|
142
|
+
# If the function printed something AND returned something, distinguish them
|
|
143
|
+
final_parts.append(f"--- RESULT ---\n{r_strip}")
|
|
144
|
+
else:
|
|
145
|
+
final_parts.append(r_strip)
|
|
146
|
+
|
|
147
|
+
result = "\n\n".join(final_parts) if final_parts else "(ok)"
|
|
148
|
+
total_lines = result.count("\n") + 1 if result else 0
|
|
149
|
+
|
|
150
|
+
# āā Audit trail: log all mutating tool operations āā
|
|
151
|
+
try:
|
|
152
|
+
if name in ("Write", "Edit", "Bash"):
|
|
153
|
+
from memory.audit import log_operation
|
|
154
|
+
log_operation(name, params, result[:200])
|
|
155
|
+
except Exception:
|
|
156
|
+
pass
|
|
157
|
+
|
|
158
|
+
# Save full un-truncated output for persistent access.
|
|
159
|
+
# Shield: tools that only READ the saved output must never overwrite it.
|
|
160
|
+
_read_only_tools = ("Read", "LineCount", "SearchLastOutput")
|
|
161
|
+
is_exploring_persistence = (
|
|
162
|
+
name == "Grep"
|
|
163
|
+
and "last_tool_output.txt" in params.get("path", "")
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
if name not in _read_only_tools and not is_exploring_persistence and not tool.display_only:
|
|
167
|
+
try:
|
|
168
|
+
global _last_seen_turn
|
|
169
|
+
curr_turn = config.get("_turn_count", -1)
|
|
170
|
+
out_file = Path.home() / ".dulus" / "last_tool_output.txt"
|
|
171
|
+
out_file.parent.mkdir(parents=True, exist_ok=True)
|
|
172
|
+
|
|
173
|
+
# If this is a new TURN (assistant turn) and it's NOT a diagnostic search,
|
|
174
|
+
# we overwrite to start fresh. Within the same turn, we append.
|
|
175
|
+
mode = "w" if curr_turn != _last_seen_turn else "a"
|
|
176
|
+
_last_seen_turn = curr_turn
|
|
177
|
+
|
|
178
|
+
with out_file.open(mode, encoding="utf-8", errors="replace") as f:
|
|
179
|
+
if mode == "a":
|
|
180
|
+
f.write(f"\n\n--- [TOOL CALL: {name}] ---\n")
|
|
181
|
+
f.write(result)
|
|
182
|
+
if mode == "a":
|
|
183
|
+
f.write("\n")
|
|
184
|
+
except Exception:
|
|
185
|
+
pass
|
|
186
|
+
|
|
187
|
+
# NO TRUNCATION for display-only tools (PrintToConsole, etc.)
|
|
188
|
+
# These tools output directly to console and don't consume context tokens
|
|
189
|
+
if not tool.display_only and len(result) > max_output:
|
|
190
|
+
total_lines = result.count("\n") + 1
|
|
191
|
+
first_chunk = max_output // 3 # Less upfront, force pagination
|
|
192
|
+
last_chunk = max_output // 6 # Even smaller tail
|
|
193
|
+
|
|
194
|
+
# Show small preview + force explicit pagination pattern
|
|
195
|
+
result = (
|
|
196
|
+
result[:first_chunk]+" >>>>>> THE RESULT WAS TRUNCATED TO AVOID TOKEN WASTE,Read last_tool_output.txt file if complete output needed <<<<<<< "+result[-last_chunk:]
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
return result
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def clear_last_output() -> None:
|
|
203
|
+
"""Reset the last_tool_output.txt file. Should be called at turn start."""
|
|
204
|
+
try:
|
|
205
|
+
out_file = Path.home() / ".dulus" / "last_tool_output.txt"
|
|
206
|
+
if out_file.exists():
|
|
207
|
+
out_file.unlink()
|
|
208
|
+
except Exception:
|
|
209
|
+
pass
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def clear_registry() -> None:
|
|
213
|
+
"""Remove all registered tools. Intended for testing."""
|
|
214
|
+
_registry.clear()
|