llmcode-cli 1.0.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.
- llm_code/__init__.py +2 -0
- llm_code/analysis/__init__.py +6 -0
- llm_code/analysis/cache.py +33 -0
- llm_code/analysis/engine.py +256 -0
- llm_code/analysis/go_rules.py +114 -0
- llm_code/analysis/js_rules.py +84 -0
- llm_code/analysis/python_rules.py +311 -0
- llm_code/analysis/rules.py +140 -0
- llm_code/analysis/rust_rules.py +108 -0
- llm_code/analysis/universal_rules.py +111 -0
- llm_code/api/__init__.py +0 -0
- llm_code/api/client.py +90 -0
- llm_code/api/errors.py +73 -0
- llm_code/api/openai_compat.py +390 -0
- llm_code/api/provider.py +35 -0
- llm_code/api/sse.py +52 -0
- llm_code/api/types.py +140 -0
- llm_code/cli/__init__.py +0 -0
- llm_code/cli/commands.py +70 -0
- llm_code/cli/image.py +122 -0
- llm_code/cli/render.py +214 -0
- llm_code/cli/status_line.py +79 -0
- llm_code/cli/streaming.py +92 -0
- llm_code/cli/tui_main.py +220 -0
- llm_code/computer_use/__init__.py +11 -0
- llm_code/computer_use/app_detect.py +49 -0
- llm_code/computer_use/app_tier.py +57 -0
- llm_code/computer_use/coordinator.py +99 -0
- llm_code/computer_use/input_control.py +71 -0
- llm_code/computer_use/screenshot.py +93 -0
- llm_code/cron/__init__.py +13 -0
- llm_code/cron/parser.py +145 -0
- llm_code/cron/scheduler.py +135 -0
- llm_code/cron/storage.py +126 -0
- llm_code/enterprise/__init__.py +1 -0
- llm_code/enterprise/audit.py +59 -0
- llm_code/enterprise/auth.py +26 -0
- llm_code/enterprise/oidc.py +95 -0
- llm_code/enterprise/rbac.py +65 -0
- llm_code/harness/__init__.py +5 -0
- llm_code/harness/config.py +33 -0
- llm_code/harness/engine.py +129 -0
- llm_code/harness/guides.py +41 -0
- llm_code/harness/sensors.py +68 -0
- llm_code/harness/templates.py +84 -0
- llm_code/hida/__init__.py +1 -0
- llm_code/hida/classifier.py +187 -0
- llm_code/hida/engine.py +49 -0
- llm_code/hida/profiles.py +95 -0
- llm_code/hida/types.py +28 -0
- llm_code/ide/__init__.py +1 -0
- llm_code/ide/bridge.py +80 -0
- llm_code/ide/detector.py +76 -0
- llm_code/ide/server.py +169 -0
- llm_code/logging.py +29 -0
- llm_code/lsp/__init__.py +0 -0
- llm_code/lsp/client.py +298 -0
- llm_code/lsp/detector.py +42 -0
- llm_code/lsp/manager.py +56 -0
- llm_code/lsp/tools.py +288 -0
- llm_code/marketplace/__init__.py +0 -0
- llm_code/marketplace/builtin_registry.py +102 -0
- llm_code/marketplace/installer.py +162 -0
- llm_code/marketplace/plugin.py +78 -0
- llm_code/marketplace/registry.py +360 -0
- llm_code/mcp/__init__.py +0 -0
- llm_code/mcp/bridge.py +87 -0
- llm_code/mcp/client.py +117 -0
- llm_code/mcp/health.py +120 -0
- llm_code/mcp/manager.py +214 -0
- llm_code/mcp/oauth.py +219 -0
- llm_code/mcp/transport.py +254 -0
- llm_code/mcp/types.py +53 -0
- llm_code/remote/__init__.py +0 -0
- llm_code/remote/client.py +136 -0
- llm_code/remote/protocol.py +22 -0
- llm_code/remote/server.py +275 -0
- llm_code/remote/ssh_proxy.py +56 -0
- llm_code/runtime/__init__.py +0 -0
- llm_code/runtime/auto_commit.py +56 -0
- llm_code/runtime/auto_diagnose.py +62 -0
- llm_code/runtime/checkpoint.py +70 -0
- llm_code/runtime/checkpoint_recovery.py +142 -0
- llm_code/runtime/compaction.py +35 -0
- llm_code/runtime/compressor.py +415 -0
- llm_code/runtime/config.py +533 -0
- llm_code/runtime/context.py +49 -0
- llm_code/runtime/conversation.py +921 -0
- llm_code/runtime/cost_tracker.py +126 -0
- llm_code/runtime/dream.py +127 -0
- llm_code/runtime/file_protection.py +150 -0
- llm_code/runtime/hardware.py +85 -0
- llm_code/runtime/hooks.py +223 -0
- llm_code/runtime/indexer.py +230 -0
- llm_code/runtime/knowledge_compiler.py +232 -0
- llm_code/runtime/memory.py +132 -0
- llm_code/runtime/memory_layers.py +467 -0
- llm_code/runtime/memory_lint.py +252 -0
- llm_code/runtime/model_aliases.py +37 -0
- llm_code/runtime/ollama.py +93 -0
- llm_code/runtime/overlay.py +124 -0
- llm_code/runtime/permissions.py +200 -0
- llm_code/runtime/plan.py +45 -0
- llm_code/runtime/prompt.py +238 -0
- llm_code/runtime/repo_map.py +174 -0
- llm_code/runtime/sandbox.py +116 -0
- llm_code/runtime/session.py +268 -0
- llm_code/runtime/skill_resolver.py +61 -0
- llm_code/runtime/skills.py +133 -0
- llm_code/runtime/speculative.py +75 -0
- llm_code/runtime/streaming_executor.py +216 -0
- llm_code/runtime/telemetry.py +196 -0
- llm_code/runtime/token_budget.py +26 -0
- llm_code/runtime/vcr.py +142 -0
- llm_code/runtime/vision.py +102 -0
- llm_code/swarm/__init__.py +1 -0
- llm_code/swarm/backend_subprocess.py +108 -0
- llm_code/swarm/backend_tmux.py +103 -0
- llm_code/swarm/backend_worktree.py +306 -0
- llm_code/swarm/checkpoint.py +74 -0
- llm_code/swarm/coordinator.py +236 -0
- llm_code/swarm/mailbox.py +88 -0
- llm_code/swarm/manager.py +202 -0
- llm_code/swarm/memory_sync.py +80 -0
- llm_code/swarm/recovery.py +21 -0
- llm_code/swarm/team.py +67 -0
- llm_code/swarm/types.py +31 -0
- llm_code/task/__init__.py +16 -0
- llm_code/task/diagnostics.py +93 -0
- llm_code/task/manager.py +162 -0
- llm_code/task/types.py +112 -0
- llm_code/task/verifier.py +104 -0
- llm_code/tools/__init__.py +0 -0
- llm_code/tools/agent.py +145 -0
- llm_code/tools/agent_roles.py +82 -0
- llm_code/tools/base.py +94 -0
- llm_code/tools/bash.py +565 -0
- llm_code/tools/computer_use_tools.py +278 -0
- llm_code/tools/coordinator_tool.py +75 -0
- llm_code/tools/cron_create.py +90 -0
- llm_code/tools/cron_delete.py +49 -0
- llm_code/tools/cron_list.py +51 -0
- llm_code/tools/deferred.py +92 -0
- llm_code/tools/dump.py +116 -0
- llm_code/tools/edit_file.py +282 -0
- llm_code/tools/git_tools.py +531 -0
- llm_code/tools/glob_search.py +112 -0
- llm_code/tools/grep_search.py +144 -0
- llm_code/tools/ide_diagnostics.py +59 -0
- llm_code/tools/ide_open.py +58 -0
- llm_code/tools/ide_selection.py +52 -0
- llm_code/tools/memory_tools.py +138 -0
- llm_code/tools/multi_edit.py +143 -0
- llm_code/tools/notebook_edit.py +107 -0
- llm_code/tools/notebook_read.py +81 -0
- llm_code/tools/parsing.py +63 -0
- llm_code/tools/read_file.py +154 -0
- llm_code/tools/registry.py +58 -0
- llm_code/tools/search_backends/__init__.py +56 -0
- llm_code/tools/search_backends/brave.py +56 -0
- llm_code/tools/search_backends/duckduckgo.py +129 -0
- llm_code/tools/search_backends/searxng.py +71 -0
- llm_code/tools/search_backends/tavily.py +73 -0
- llm_code/tools/swarm_create.py +109 -0
- llm_code/tools/swarm_delete.py +95 -0
- llm_code/tools/swarm_list.py +44 -0
- llm_code/tools/swarm_message.py +109 -0
- llm_code/tools/task_close.py +79 -0
- llm_code/tools/task_plan.py +79 -0
- llm_code/tools/task_verify.py +90 -0
- llm_code/tools/tool_search.py +65 -0
- llm_code/tools/web_common.py +258 -0
- llm_code/tools/web_fetch.py +223 -0
- llm_code/tools/web_search.py +280 -0
- llm_code/tools/write_file.py +118 -0
- llm_code/tui/__init__.py +1 -0
- llm_code/tui/app.py +2432 -0
- llm_code/tui/chat_view.py +82 -0
- llm_code/tui/chat_widgets.py +309 -0
- llm_code/tui/header_bar.py +46 -0
- llm_code/tui/input_bar.py +349 -0
- llm_code/tui/keybindings.py +142 -0
- llm_code/tui/marketplace.py +210 -0
- llm_code/tui/status_bar.py +72 -0
- llm_code/tui/theme.py +96 -0
- llm_code/utils/__init__.py +0 -0
- llm_code/utils/diff.py +111 -0
- llm_code/utils/errors.py +70 -0
- llm_code/utils/hyperlink.py +73 -0
- llm_code/utils/notebook.py +179 -0
- llm_code/utils/search.py +69 -0
- llm_code/utils/text_normalize.py +28 -0
- llm_code/utils/version_check.py +62 -0
- llm_code/vim/__init__.py +4 -0
- llm_code/vim/engine.py +51 -0
- llm_code/vim/motions.py +172 -0
- llm_code/vim/operators.py +183 -0
- llm_code/vim/text_objects.py +139 -0
- llm_code/vim/transitions.py +279 -0
- llm_code/vim/types.py +68 -0
- llm_code/voice/__init__.py +1 -0
- llm_code/voice/languages.py +43 -0
- llm_code/voice/recorder.py +136 -0
- llm_code/voice/stt.py +36 -0
- llm_code/voice/stt_anthropic.py +66 -0
- llm_code/voice/stt_google.py +32 -0
- llm_code/voice/stt_whisper.py +52 -0
- llmcode_cli-1.0.0.dist-info/METADATA +524 -0
- llmcode_cli-1.0.0.dist-info/RECORD +212 -0
- llmcode_cli-1.0.0.dist-info/WHEEL +4 -0
- llmcode_cli-1.0.0.dist-info/entry_points.txt +2 -0
- llmcode_cli-1.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
"""Tool classes for computer use (GUI automation)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from llm_code.tools.base import PermissionLevel, Tool, ToolResult
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from llm_code.runtime.config import ComputerUseConfig
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class _ComputerUseTool(Tool):
|
|
14
|
+
"""Base class for computer-use tools with shared config check."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, config: "ComputerUseConfig") -> None:
|
|
17
|
+
from llm_code.computer_use.coordinator import ComputerUseCoordinator
|
|
18
|
+
self._config = config
|
|
19
|
+
self._coordinator = ComputerUseCoordinator(config)
|
|
20
|
+
|
|
21
|
+
def _check_enabled(self) -> ToolResult | None:
|
|
22
|
+
if not self._config.enabled:
|
|
23
|
+
return ToolResult(
|
|
24
|
+
output="Computer use is not enabled. Set computer_use.enabled=true in config.",
|
|
25
|
+
is_error=True,
|
|
26
|
+
)
|
|
27
|
+
return None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ScreenshotTool(_ComputerUseTool):
|
|
31
|
+
@property
|
|
32
|
+
def name(self) -> str:
|
|
33
|
+
return "screenshot"
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def description(self) -> str:
|
|
37
|
+
return "Take a screenshot of the current screen. Returns a base64-encoded PNG image."
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def input_schema(self) -> dict:
|
|
41
|
+
return {
|
|
42
|
+
"type": "object",
|
|
43
|
+
"properties": {},
|
|
44
|
+
"additionalProperties": False,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def required_permission(self) -> PermissionLevel:
|
|
49
|
+
return PermissionLevel.READ_ONLY
|
|
50
|
+
|
|
51
|
+
def is_read_only(self, args: dict) -> bool:
|
|
52
|
+
return True
|
|
53
|
+
|
|
54
|
+
def execute(self, args: dict) -> ToolResult:
|
|
55
|
+
err = self._check_enabled()
|
|
56
|
+
if err:
|
|
57
|
+
return err
|
|
58
|
+
try:
|
|
59
|
+
result = self._coordinator.screenshot()
|
|
60
|
+
return ToolResult(
|
|
61
|
+
output=json.dumps(result),
|
|
62
|
+
metadata={"has_image": True},
|
|
63
|
+
)
|
|
64
|
+
except Exception as exc:
|
|
65
|
+
return ToolResult(output=f"Screenshot failed: {exc}", is_error=True)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class MouseClickTool(_ComputerUseTool):
|
|
69
|
+
@property
|
|
70
|
+
def name(self) -> str:
|
|
71
|
+
return "mouse_click"
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def description(self) -> str:
|
|
75
|
+
return "Click the mouse at (x, y) coordinates. Returns a screenshot after clicking."
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def input_schema(self) -> dict:
|
|
79
|
+
return {
|
|
80
|
+
"type": "object",
|
|
81
|
+
"properties": {
|
|
82
|
+
"x": {"type": "integer", "description": "X coordinate"},
|
|
83
|
+
"y": {"type": "integer", "description": "Y coordinate"},
|
|
84
|
+
"button": {
|
|
85
|
+
"type": "string",
|
|
86
|
+
"enum": ["left", "right", "middle"],
|
|
87
|
+
"default": "left",
|
|
88
|
+
"description": "Mouse button",
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
"required": ["x", "y"],
|
|
92
|
+
"additionalProperties": False,
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def required_permission(self) -> PermissionLevel:
|
|
97
|
+
return PermissionLevel.FULL_ACCESS
|
|
98
|
+
|
|
99
|
+
def execute(self, args: dict) -> ToolResult:
|
|
100
|
+
err = self._check_enabled()
|
|
101
|
+
if err:
|
|
102
|
+
return err
|
|
103
|
+
try:
|
|
104
|
+
result = self._coordinator.click_and_observe(
|
|
105
|
+
x=args["x"],
|
|
106
|
+
y=args["y"],
|
|
107
|
+
button=args.get("button", "left"),
|
|
108
|
+
)
|
|
109
|
+
return ToolResult(output=json.dumps(result), metadata={"has_image": True})
|
|
110
|
+
except Exception as exc:
|
|
111
|
+
return ToolResult(output=f"Mouse click failed: {exc}", is_error=True)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class KeyboardTypeTool(_ComputerUseTool):
|
|
115
|
+
@property
|
|
116
|
+
def name(self) -> str:
|
|
117
|
+
return "keyboard_type"
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def description(self) -> str:
|
|
121
|
+
return "Type text using the keyboard. Returns a screenshot after typing."
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def input_schema(self) -> dict:
|
|
125
|
+
return {
|
|
126
|
+
"type": "object",
|
|
127
|
+
"properties": {
|
|
128
|
+
"text": {"type": "string", "description": "Text to type"},
|
|
129
|
+
},
|
|
130
|
+
"required": ["text"],
|
|
131
|
+
"additionalProperties": False,
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
@property
|
|
135
|
+
def required_permission(self) -> PermissionLevel:
|
|
136
|
+
return PermissionLevel.FULL_ACCESS
|
|
137
|
+
|
|
138
|
+
def execute(self, args: dict) -> ToolResult:
|
|
139
|
+
err = self._check_enabled()
|
|
140
|
+
if err:
|
|
141
|
+
return err
|
|
142
|
+
try:
|
|
143
|
+
result = self._coordinator.type_and_observe(args["text"])
|
|
144
|
+
return ToolResult(output=json.dumps(result), metadata={"has_image": True})
|
|
145
|
+
except Exception as exc:
|
|
146
|
+
return ToolResult(output=f"Keyboard type failed: {exc}", is_error=True)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class KeyPressTool(_ComputerUseTool):
|
|
150
|
+
@property
|
|
151
|
+
def name(self) -> str:
|
|
152
|
+
return "key_press"
|
|
153
|
+
|
|
154
|
+
@property
|
|
155
|
+
def description(self) -> str:
|
|
156
|
+
return "Press a keyboard shortcut (e.g., ctrl+c). Returns a screenshot after pressing."
|
|
157
|
+
|
|
158
|
+
@property
|
|
159
|
+
def input_schema(self) -> dict:
|
|
160
|
+
return {
|
|
161
|
+
"type": "object",
|
|
162
|
+
"properties": {
|
|
163
|
+
"keys": {
|
|
164
|
+
"type": "array",
|
|
165
|
+
"items": {"type": "string"},
|
|
166
|
+
"description": "Keys to press simultaneously (e.g., ['ctrl', 'c'])",
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
"required": ["keys"],
|
|
170
|
+
"additionalProperties": False,
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
@property
|
|
174
|
+
def required_permission(self) -> PermissionLevel:
|
|
175
|
+
return PermissionLevel.FULL_ACCESS
|
|
176
|
+
|
|
177
|
+
def execute(self, args: dict) -> ToolResult:
|
|
178
|
+
err = self._check_enabled()
|
|
179
|
+
if err:
|
|
180
|
+
return err
|
|
181
|
+
try:
|
|
182
|
+
result = self._coordinator.hotkey_and_observe(*args["keys"])
|
|
183
|
+
return ToolResult(output=json.dumps(result), metadata={"has_image": True})
|
|
184
|
+
except Exception as exc:
|
|
185
|
+
return ToolResult(output=f"Key press failed: {exc}", is_error=True)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class ScrollTool(_ComputerUseTool):
|
|
189
|
+
@property
|
|
190
|
+
def name(self) -> str:
|
|
191
|
+
return "scroll"
|
|
192
|
+
|
|
193
|
+
@property
|
|
194
|
+
def description(self) -> str:
|
|
195
|
+
return "Scroll the mouse wheel. Positive clicks = up, negative = down. Returns a screenshot."
|
|
196
|
+
|
|
197
|
+
@property
|
|
198
|
+
def input_schema(self) -> dict:
|
|
199
|
+
return {
|
|
200
|
+
"type": "object",
|
|
201
|
+
"properties": {
|
|
202
|
+
"clicks": {
|
|
203
|
+
"type": "integer",
|
|
204
|
+
"description": "Scroll amount (positive=up, negative=down)",
|
|
205
|
+
},
|
|
206
|
+
"x": {"type": "integer", "description": "Optional X position"},
|
|
207
|
+
"y": {"type": "integer", "description": "Optional Y position"},
|
|
208
|
+
},
|
|
209
|
+
"required": ["clicks"],
|
|
210
|
+
"additionalProperties": False,
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
@property
|
|
214
|
+
def required_permission(self) -> PermissionLevel:
|
|
215
|
+
return PermissionLevel.FULL_ACCESS
|
|
216
|
+
|
|
217
|
+
def execute(self, args: dict) -> ToolResult:
|
|
218
|
+
err = self._check_enabled()
|
|
219
|
+
if err:
|
|
220
|
+
return err
|
|
221
|
+
try:
|
|
222
|
+
result = self._coordinator.scroll_and_observe(
|
|
223
|
+
clicks=args["clicks"],
|
|
224
|
+
x=args.get("x"),
|
|
225
|
+
y=args.get("y"),
|
|
226
|
+
)
|
|
227
|
+
return ToolResult(output=json.dumps(result), metadata={"has_image": True})
|
|
228
|
+
except Exception as exc:
|
|
229
|
+
return ToolResult(output=f"Scroll failed: {exc}", is_error=True)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
class MouseDragTool(_ComputerUseTool):
|
|
233
|
+
@property
|
|
234
|
+
def name(self) -> str:
|
|
235
|
+
return "mouse_drag"
|
|
236
|
+
|
|
237
|
+
@property
|
|
238
|
+
def description(self) -> str:
|
|
239
|
+
return "Drag the mouse from a start position by an offset. Returns a screenshot."
|
|
240
|
+
|
|
241
|
+
@property
|
|
242
|
+
def input_schema(self) -> dict:
|
|
243
|
+
return {
|
|
244
|
+
"type": "object",
|
|
245
|
+
"properties": {
|
|
246
|
+
"start_x": {"type": "integer", "description": "Start X coordinate"},
|
|
247
|
+
"start_y": {"type": "integer", "description": "Start Y coordinate"},
|
|
248
|
+
"offset_x": {"type": "integer", "description": "Horizontal drag distance"},
|
|
249
|
+
"offset_y": {"type": "integer", "description": "Vertical drag distance"},
|
|
250
|
+
"duration": {
|
|
251
|
+
"type": "number",
|
|
252
|
+
"default": 0.5,
|
|
253
|
+
"description": "Drag duration in seconds",
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
"required": ["start_x", "start_y", "offset_x", "offset_y"],
|
|
257
|
+
"additionalProperties": False,
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
@property
|
|
261
|
+
def required_permission(self) -> PermissionLevel:
|
|
262
|
+
return PermissionLevel.FULL_ACCESS
|
|
263
|
+
|
|
264
|
+
def execute(self, args: dict) -> ToolResult:
|
|
265
|
+
err = self._check_enabled()
|
|
266
|
+
if err:
|
|
267
|
+
return err
|
|
268
|
+
try:
|
|
269
|
+
result = self._coordinator.drag_and_observe(
|
|
270
|
+
start_x=args["start_x"],
|
|
271
|
+
start_y=args["start_y"],
|
|
272
|
+
offset_x=args["offset_x"],
|
|
273
|
+
offset_y=args["offset_y"],
|
|
274
|
+
duration=args.get("duration", 0.5),
|
|
275
|
+
)
|
|
276
|
+
return ToolResult(output=json.dumps(result), metadata={"has_image": True})
|
|
277
|
+
except Exception as exc:
|
|
278
|
+
return ToolResult(output=f"Mouse drag failed: {exc}", is_error=True)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""CoordinatorTool — auto-decompose and delegate a task to swarm workers."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
import concurrent.futures
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
|
|
9
|
+
from llm_code.tools.base import PermissionLevel, Tool, ToolResult
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CoordinatorInput(BaseModel):
|
|
13
|
+
task: str
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CoordinatorTool(Tool):
|
|
17
|
+
"""Tool that auto-decomposes a high-level task and dispatches to swarm workers."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, coordinator: object) -> None:
|
|
20
|
+
self._coordinator = coordinator
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def name(self) -> str:
|
|
24
|
+
return "coordinate"
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def description(self) -> str:
|
|
28
|
+
return (
|
|
29
|
+
"Auto-decompose a high-level task into subtasks and delegate each one "
|
|
30
|
+
"to a separate swarm worker agent. The coordinator monitors progress "
|
|
31
|
+
"and returns an aggregated summary when all workers finish (or timeout)."
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def input_schema(self) -> dict:
|
|
36
|
+
return {
|
|
37
|
+
"type": "object",
|
|
38
|
+
"properties": {
|
|
39
|
+
"task": {
|
|
40
|
+
"type": "string",
|
|
41
|
+
"description": "High-level task to decompose and delegate to worker agents.",
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
"required": ["task"],
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def required_permission(self) -> PermissionLevel:
|
|
49
|
+
return PermissionLevel.FULL_ACCESS
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def input_model(self) -> type[CoordinatorInput]:
|
|
53
|
+
return CoordinatorInput
|
|
54
|
+
|
|
55
|
+
def execute(self, args: dict) -> ToolResult:
|
|
56
|
+
task = args["task"]
|
|
57
|
+
try:
|
|
58
|
+
try:
|
|
59
|
+
asyncio.get_running_loop()
|
|
60
|
+
running = True
|
|
61
|
+
except RuntimeError:
|
|
62
|
+
running = False
|
|
63
|
+
|
|
64
|
+
if running:
|
|
65
|
+
with concurrent.futures.ThreadPoolExecutor() as pool:
|
|
66
|
+
result = pool.submit(
|
|
67
|
+
asyncio.run,
|
|
68
|
+
self._coordinator.orchestrate(task),
|
|
69
|
+
).result()
|
|
70
|
+
else:
|
|
71
|
+
result = asyncio.run(self._coordinator.orchestrate(task))
|
|
72
|
+
|
|
73
|
+
return ToolResult(output=result)
|
|
74
|
+
except Exception as exc:
|
|
75
|
+
return ToolResult(output=f"Coordinator error: {exc}", is_error=True)
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""CronCreateTool — schedule a new cron task."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
|
|
6
|
+
from llm_code.cron.parser import parse_cron
|
|
7
|
+
from llm_code.cron.storage import CronStorage
|
|
8
|
+
from llm_code.tools.base import PermissionLevel, Tool, ToolResult
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CronCreateInput(BaseModel):
|
|
12
|
+
cron: str
|
|
13
|
+
prompt: str
|
|
14
|
+
recurring: bool = True
|
|
15
|
+
permanent: bool = False
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class CronCreateTool(Tool):
|
|
19
|
+
def __init__(self, storage: CronStorage) -> None:
|
|
20
|
+
self._storage = storage
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def name(self) -> str:
|
|
24
|
+
return "cron_create"
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def description(self) -> str:
|
|
28
|
+
return (
|
|
29
|
+
"Schedule a prompt to run on a cron schedule. "
|
|
30
|
+
"5-field format: minute hour day-of-month month day-of-week (local time). "
|
|
31
|
+
"recurring=True keeps firing; permanent=True prevents 30-day auto-expiry."
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def input_schema(self) -> dict:
|
|
36
|
+
return {
|
|
37
|
+
"type": "object",
|
|
38
|
+
"properties": {
|
|
39
|
+
"cron": {"type": "string", "description": "Cron expression (5-field)"},
|
|
40
|
+
"prompt": {"type": "string", "description": "Prompt to execute when fired"},
|
|
41
|
+
"recurring": {"type": "boolean", "description": "Keep firing (default true)", "default": True},
|
|
42
|
+
"permanent": {"type": "boolean", "description": "Prevent 30-day auto-expiry (default false)", "default": False},
|
|
43
|
+
},
|
|
44
|
+
"required": ["cron", "prompt"],
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def required_permission(self) -> PermissionLevel:
|
|
49
|
+
return PermissionLevel.FULL_ACCESS
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def input_model(self) -> type[CronCreateInput]:
|
|
53
|
+
return CronCreateInput
|
|
54
|
+
|
|
55
|
+
def execute(self, args: dict) -> ToolResult:
|
|
56
|
+
cron_expr = args["cron"]
|
|
57
|
+
prompt = args["prompt"]
|
|
58
|
+
recurring = args.get("recurring", True)
|
|
59
|
+
permanent = args.get("permanent", False)
|
|
60
|
+
|
|
61
|
+
# Validate cron expression
|
|
62
|
+
try:
|
|
63
|
+
parse_cron(cron_expr)
|
|
64
|
+
except ValueError as exc:
|
|
65
|
+
return ToolResult(output=f"Invalid cron expression: {exc}", is_error=True)
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
task = self._storage.add(
|
|
69
|
+
cron=cron_expr,
|
|
70
|
+
prompt=prompt,
|
|
71
|
+
recurring=recurring,
|
|
72
|
+
permanent=permanent,
|
|
73
|
+
)
|
|
74
|
+
except ValueError as exc:
|
|
75
|
+
return ToolResult(output=str(exc), is_error=True)
|
|
76
|
+
|
|
77
|
+
flags = []
|
|
78
|
+
if recurring:
|
|
79
|
+
flags.append("recurring")
|
|
80
|
+
if permanent:
|
|
81
|
+
flags.append("permanent")
|
|
82
|
+
flag_str = f" ({', '.join(flags)})" if flags else ""
|
|
83
|
+
|
|
84
|
+
return ToolResult(
|
|
85
|
+
output=(
|
|
86
|
+
f"Scheduled task {task.id}{flag_str}\n"
|
|
87
|
+
f" Cron: {task.cron}\n"
|
|
88
|
+
f" Prompt: {task.prompt}"
|
|
89
|
+
)
|
|
90
|
+
)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""CronDeleteTool — delete a scheduled cron task."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
|
|
6
|
+
from llm_code.cron.storage import CronStorage
|
|
7
|
+
from llm_code.tools.base import PermissionLevel, Tool, ToolResult
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class CronDeleteInput(BaseModel):
|
|
11
|
+
task_id: str
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CronDeleteTool(Tool):
|
|
15
|
+
def __init__(self, storage: CronStorage) -> None:
|
|
16
|
+
self._storage = storage
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def name(self) -> str:
|
|
20
|
+
return "cron_delete"
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def description(self) -> str:
|
|
24
|
+
return "Delete a scheduled cron task by ID."
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def input_schema(self) -> dict:
|
|
28
|
+
return {
|
|
29
|
+
"type": "object",
|
|
30
|
+
"properties": {
|
|
31
|
+
"task_id": {"type": "string", "description": "ID of the task to delete"},
|
|
32
|
+
},
|
|
33
|
+
"required": ["task_id"],
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def required_permission(self) -> PermissionLevel:
|
|
38
|
+
return PermissionLevel.FULL_ACCESS
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def input_model(self) -> type[CronDeleteInput]:
|
|
42
|
+
return CronDeleteInput
|
|
43
|
+
|
|
44
|
+
def execute(self, args: dict) -> ToolResult:
|
|
45
|
+
task_id = args["task_id"]
|
|
46
|
+
removed = self._storage.remove(task_id)
|
|
47
|
+
if not removed:
|
|
48
|
+
return ToolResult(output=f"Task '{task_id}' not found", is_error=True)
|
|
49
|
+
return ToolResult(output=f"Deleted task {task_id}")
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""CronListTool — list all scheduled cron tasks."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from llm_code.cron.storage import CronStorage
|
|
5
|
+
from llm_code.tools.base import PermissionLevel, Tool, ToolResult
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CronListTool(Tool):
|
|
9
|
+
def __init__(self, storage: CronStorage) -> None:
|
|
10
|
+
self._storage = storage
|
|
11
|
+
|
|
12
|
+
@property
|
|
13
|
+
def name(self) -> str:
|
|
14
|
+
return "cron_list"
|
|
15
|
+
|
|
16
|
+
@property
|
|
17
|
+
def description(self) -> str:
|
|
18
|
+
return "List all scheduled cron tasks with their status."
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def input_schema(self) -> dict:
|
|
22
|
+
return {"type": "object", "properties": {}}
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def required_permission(self) -> PermissionLevel:
|
|
26
|
+
return PermissionLevel.READ_ONLY
|
|
27
|
+
|
|
28
|
+
def is_read_only(self, args: dict) -> bool:
|
|
29
|
+
return True
|
|
30
|
+
|
|
31
|
+
def is_concurrency_safe(self, args: dict) -> bool:
|
|
32
|
+
return True
|
|
33
|
+
|
|
34
|
+
def execute(self, args: dict) -> ToolResult:
|
|
35
|
+
tasks = self._storage.list_all()
|
|
36
|
+
if not tasks:
|
|
37
|
+
return ToolResult(output="No scheduled tasks.")
|
|
38
|
+
|
|
39
|
+
lines: list[str] = [f"Scheduled tasks ({len(tasks)}):"]
|
|
40
|
+
for t in tasks:
|
|
41
|
+
flags = []
|
|
42
|
+
if t.recurring:
|
|
43
|
+
flags.append("recurring")
|
|
44
|
+
if t.permanent:
|
|
45
|
+
flags.append("permanent")
|
|
46
|
+
flag_str = f" [{', '.join(flags)}]" if flags else ""
|
|
47
|
+
fired = f", last fired: {t.last_fired_at:%Y-%m-%d %H:%M}" if t.last_fired_at else ""
|
|
48
|
+
lines.append(
|
|
49
|
+
f" {t.id} {t.cron} \"{t.prompt}\"{flag_str}{fired}"
|
|
50
|
+
)
|
|
51
|
+
return ToolResult(output="\n".join(lines))
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Deferred tool loading: keeps visible tool list small by hiding rarely-used tools."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from llm_code.api.types import ToolDefinition
|
|
5
|
+
|
|
6
|
+
# Core tools always visible regardless of max_visible limit
|
|
7
|
+
CORE_TOOLS: frozenset[str] = frozenset(
|
|
8
|
+
{
|
|
9
|
+
"read_file",
|
|
10
|
+
"write_file",
|
|
11
|
+
"edit_file",
|
|
12
|
+
"glob_search",
|
|
13
|
+
"grep_search",
|
|
14
|
+
"bash",
|
|
15
|
+
"agent",
|
|
16
|
+
"tool_search",
|
|
17
|
+
}
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class DeferredToolManager:
|
|
22
|
+
"""Manages splitting tool definitions into visible and deferred sets.
|
|
23
|
+
|
|
24
|
+
Core tools are always visible. Additional tools are shown up to max_visible;
|
|
25
|
+
the rest are deferred until the LLM calls tool_search to unlock them.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self) -> None:
|
|
29
|
+
self._unlocked: set[str] = set()
|
|
30
|
+
# Track the last deferred list for search
|
|
31
|
+
self._deferred: list[ToolDefinition] = []
|
|
32
|
+
|
|
33
|
+
def select_tools(
|
|
34
|
+
self,
|
|
35
|
+
all_defs: list[ToolDefinition],
|
|
36
|
+
max_visible: int = 20,
|
|
37
|
+
) -> tuple[list[ToolDefinition], list[ToolDefinition]]:
|
|
38
|
+
"""Partition tool definitions into (visible, deferred).
|
|
39
|
+
|
|
40
|
+
Core tools and any unlocked tools are always visible. Non-core tools
|
|
41
|
+
fill remaining slots up to max_visible; the rest go to deferred.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
A 2-tuple of (visible_defs, deferred_defs).
|
|
45
|
+
"""
|
|
46
|
+
all_names = {d.name for d in all_defs}
|
|
47
|
+
|
|
48
|
+
# Always-visible: core tools present in all_defs + previously unlocked
|
|
49
|
+
always_visible_names = (CORE_TOOLS | self._unlocked) & all_names
|
|
50
|
+
|
|
51
|
+
visible: list[ToolDefinition] = []
|
|
52
|
+
deferred: list[ToolDefinition] = []
|
|
53
|
+
|
|
54
|
+
for d in all_defs:
|
|
55
|
+
if d.name in always_visible_names:
|
|
56
|
+
visible.append(d)
|
|
57
|
+
|
|
58
|
+
# Fill remaining slots with non-core, non-unlocked tools
|
|
59
|
+
remaining_slots = max_visible - len(visible)
|
|
60
|
+
for d in all_defs:
|
|
61
|
+
if d.name not in always_visible_names:
|
|
62
|
+
if remaining_slots > 0:
|
|
63
|
+
visible.append(d)
|
|
64
|
+
remaining_slots -= 1
|
|
65
|
+
else:
|
|
66
|
+
deferred.append(d)
|
|
67
|
+
|
|
68
|
+
# Store for search
|
|
69
|
+
self._deferred = deferred
|
|
70
|
+
return visible, deferred
|
|
71
|
+
|
|
72
|
+
def search_tools(
|
|
73
|
+
self, query: str, deferred: list[ToolDefinition]
|
|
74
|
+
) -> list[ToolDefinition]:
|
|
75
|
+
"""Fuzzy-match query against name and description of deferred tools.
|
|
76
|
+
|
|
77
|
+
Matching is case-insensitive substring search against both the tool
|
|
78
|
+
name and description.
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
List of matching ToolDefinition objects.
|
|
82
|
+
"""
|
|
83
|
+
q = query.lower()
|
|
84
|
+
results: list[ToolDefinition] = []
|
|
85
|
+
for d in deferred:
|
|
86
|
+
if q in d.name.lower() or q in d.description.lower():
|
|
87
|
+
results.append(d)
|
|
88
|
+
return results
|
|
89
|
+
|
|
90
|
+
def unlock_tool(self, name: str) -> None:
|
|
91
|
+
"""Mark a tool as unlocked so it appears in visible set on future calls."""
|
|
92
|
+
self._unlocked.add(name)
|