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,129 @@
|
|
|
1
|
+
"""DuckDuckGo Lite search backend."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import time
|
|
5
|
+
from html.parser import HTMLParser
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from llm_code.tools.search_backends import SearchResult
|
|
10
|
+
|
|
11
|
+
_DDG_LITE_URL = "https://lite.duckduckgo.com/lite/"
|
|
12
|
+
_RATE_LIMIT_SECONDS = 1.0
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class _DDGLiteParser(HTMLParser):
|
|
16
|
+
"""Parse DuckDuckGo Lite HTML to extract search results."""
|
|
17
|
+
|
|
18
|
+
def __init__(self) -> None:
|
|
19
|
+
super().__init__()
|
|
20
|
+
self._results: list[dict[str, str]] = []
|
|
21
|
+
self._in_title_link = False
|
|
22
|
+
self._in_snippet = False
|
|
23
|
+
self._current_url = ""
|
|
24
|
+
self._current_title = ""
|
|
25
|
+
self._snippet_depth = 0
|
|
26
|
+
|
|
27
|
+
def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
|
|
28
|
+
attr_dict = dict(attrs)
|
|
29
|
+
classes = attr_dict.get("class", "") or ""
|
|
30
|
+
|
|
31
|
+
if tag == "a" and "result__a" in classes:
|
|
32
|
+
self._in_title_link = True
|
|
33
|
+
self._current_url = attr_dict.get("href", "")
|
|
34
|
+
self._current_title = ""
|
|
35
|
+
|
|
36
|
+
if tag == "div" and "result__snippet" in classes:
|
|
37
|
+
self._in_snippet = True
|
|
38
|
+
self._snippet_depth = 1
|
|
39
|
+
self._current_snippet = ""
|
|
40
|
+
|
|
41
|
+
if self._in_snippet and tag != "div":
|
|
42
|
+
pass # track inner tags
|
|
43
|
+
|
|
44
|
+
def handle_endtag(self, tag: str) -> None:
|
|
45
|
+
if self._in_title_link and tag == "a":
|
|
46
|
+
self._in_title_link = False
|
|
47
|
+
|
|
48
|
+
if self._in_snippet and tag == "div":
|
|
49
|
+
self._in_snippet = False
|
|
50
|
+
# Save result when snippet ends
|
|
51
|
+
if self._current_url and self._current_title:
|
|
52
|
+
self._results.append({
|
|
53
|
+
"title": self._current_title.strip(),
|
|
54
|
+
"url": self._current_url,
|
|
55
|
+
"snippet": getattr(self, "_current_snippet", "").strip(),
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
def handle_data(self, data: str) -> None:
|
|
59
|
+
if self._in_title_link:
|
|
60
|
+
self._current_title += data
|
|
61
|
+
if self._in_snippet:
|
|
62
|
+
self._current_snippet += data
|
|
63
|
+
|
|
64
|
+
def get_results(self) -> list[dict[str, str]]:
|
|
65
|
+
return self._results
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class DuckDuckGoBackend:
|
|
69
|
+
"""Search backend using DuckDuckGo Lite."""
|
|
70
|
+
|
|
71
|
+
def __init__(self) -> None:
|
|
72
|
+
self._last_request_time: float = 0.0
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def name(self) -> str:
|
|
76
|
+
return "duckduckgo"
|
|
77
|
+
|
|
78
|
+
def _rate_limit(self) -> None:
|
|
79
|
+
"""Enforce 1-second rate limit between requests."""
|
|
80
|
+
elapsed = time.monotonic() - self._last_request_time
|
|
81
|
+
if elapsed < _RATE_LIMIT_SECONDS:
|
|
82
|
+
time.sleep(_RATE_LIMIT_SECONDS - elapsed)
|
|
83
|
+
self._last_request_time = time.monotonic()
|
|
84
|
+
|
|
85
|
+
def search(self, query: str, *, max_results: int = 10) -> tuple[SearchResult, ...]:
|
|
86
|
+
"""Search DuckDuckGo Lite and return results.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
query: Search query string.
|
|
90
|
+
max_results: Maximum number of results to return.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
Tuple of SearchResult, or empty tuple on error.
|
|
94
|
+
"""
|
|
95
|
+
self._rate_limit()
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
response = httpx.get(
|
|
99
|
+
_DDG_LITE_URL,
|
|
100
|
+
params={"q": query},
|
|
101
|
+
headers={
|
|
102
|
+
"User-Agent": (
|
|
103
|
+
"Mozilla/5.0 (compatible; llm-code/1.0; "
|
|
104
|
+
"+https://github.com/llm-code)"
|
|
105
|
+
)
|
|
106
|
+
},
|
|
107
|
+
timeout=10.0,
|
|
108
|
+
follow_redirects=True,
|
|
109
|
+
)
|
|
110
|
+
except httpx.RequestError:
|
|
111
|
+
return ()
|
|
112
|
+
|
|
113
|
+
if response.status_code != 200:
|
|
114
|
+
return ()
|
|
115
|
+
|
|
116
|
+
parser = _DDGLiteParser()
|
|
117
|
+
parser.feed(response.text)
|
|
118
|
+
raw = parser.get_results()
|
|
119
|
+
|
|
120
|
+
results = tuple(
|
|
121
|
+
SearchResult(
|
|
122
|
+
title=r["title"],
|
|
123
|
+
url=r["url"],
|
|
124
|
+
snippet=r["snippet"],
|
|
125
|
+
)
|
|
126
|
+
for r in raw[:max_results]
|
|
127
|
+
if r.get("title") and r.get("url")
|
|
128
|
+
)
|
|
129
|
+
return results
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""SearXNG search backend."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import httpx
|
|
5
|
+
|
|
6
|
+
from llm_code.tools.search_backends import SearchResult
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SearXNGBackend:
|
|
10
|
+
"""Search backend using a self-hosted SearXNG instance."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, base_url: str) -> None:
|
|
13
|
+
"""Initialize with SearXNG instance base URL.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
base_url: Base URL of SearXNG instance (e.g. http://localhost:8080).
|
|
17
|
+
|
|
18
|
+
Raises:
|
|
19
|
+
ValueError: If base_url is empty or whitespace.
|
|
20
|
+
"""
|
|
21
|
+
if not base_url or not base_url.strip():
|
|
22
|
+
raise ValueError("base_url must not be empty")
|
|
23
|
+
self._base_url = base_url.rstrip("/")
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def name(self) -> str:
|
|
27
|
+
return "searxng"
|
|
28
|
+
|
|
29
|
+
def search(self, query: str, *, max_results: int = 10) -> tuple[SearchResult, ...]:
|
|
30
|
+
"""Search via SearXNG JSON API.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
query: Search query string.
|
|
34
|
+
max_results: Maximum number of results to return.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
Tuple of SearchResult, or empty tuple on error.
|
|
38
|
+
"""
|
|
39
|
+
search_url = f"{self._base_url}/search"
|
|
40
|
+
try:
|
|
41
|
+
response = httpx.get(
|
|
42
|
+
search_url,
|
|
43
|
+
params={
|
|
44
|
+
"q": query,
|
|
45
|
+
"format": "json",
|
|
46
|
+
"pageno": 1,
|
|
47
|
+
},
|
|
48
|
+
timeout=15.0,
|
|
49
|
+
)
|
|
50
|
+
except httpx.RequestError:
|
|
51
|
+
return ()
|
|
52
|
+
|
|
53
|
+
if response.status_code != 200:
|
|
54
|
+
return ()
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
data = response.json()
|
|
58
|
+
except Exception:
|
|
59
|
+
return ()
|
|
60
|
+
|
|
61
|
+
raw_results = data.get("results", [])
|
|
62
|
+
results = tuple(
|
|
63
|
+
SearchResult(
|
|
64
|
+
title=r.get("title", ""),
|
|
65
|
+
url=r.get("url", ""),
|
|
66
|
+
snippet=r.get("content", ""),
|
|
67
|
+
)
|
|
68
|
+
for r in raw_results[:max_results]
|
|
69
|
+
if r.get("url")
|
|
70
|
+
)
|
|
71
|
+
return results
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Tavily search backend."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import httpx
|
|
5
|
+
|
|
6
|
+
from llm_code.tools.search_backends import SearchResult
|
|
7
|
+
|
|
8
|
+
_TAVILY_SEARCH_URL = "https://api.tavily.com/search"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TavilyBackend:
|
|
12
|
+
"""Search backend using Tavily API."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, api_key: str) -> None:
|
|
15
|
+
"""Initialize with Tavily API key.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
api_key: Tavily API key.
|
|
19
|
+
|
|
20
|
+
Raises:
|
|
21
|
+
ValueError: If api_key is empty or whitespace.
|
|
22
|
+
"""
|
|
23
|
+
if not api_key or not api_key.strip():
|
|
24
|
+
raise ValueError("api_key must not be empty")
|
|
25
|
+
self._api_key = api_key
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def name(self) -> str:
|
|
29
|
+
return "tavily"
|
|
30
|
+
|
|
31
|
+
def search(self, query: str, *, max_results: int = 10) -> tuple[SearchResult, ...]:
|
|
32
|
+
"""Search via Tavily API.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
query: Search query string.
|
|
36
|
+
max_results: Maximum number of results to return.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
Tuple of SearchResult, or empty tuple on error.
|
|
40
|
+
"""
|
|
41
|
+
try:
|
|
42
|
+
response = httpx.post(
|
|
43
|
+
_TAVILY_SEARCH_URL,
|
|
44
|
+
json={
|
|
45
|
+
"api_key": self._api_key,
|
|
46
|
+
"query": query,
|
|
47
|
+
"max_results": max_results,
|
|
48
|
+
"search_depth": "basic",
|
|
49
|
+
},
|
|
50
|
+
timeout=15.0,
|
|
51
|
+
)
|
|
52
|
+
except httpx.RequestError:
|
|
53
|
+
return ()
|
|
54
|
+
|
|
55
|
+
if response.status_code != 200:
|
|
56
|
+
return ()
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
data = response.json()
|
|
60
|
+
except Exception:
|
|
61
|
+
return ()
|
|
62
|
+
|
|
63
|
+
raw_results = data.get("results", [])
|
|
64
|
+
results = tuple(
|
|
65
|
+
SearchResult(
|
|
66
|
+
title=r.get("title", ""),
|
|
67
|
+
url=r.get("url", ""),
|
|
68
|
+
snippet=r.get("content", ""),
|
|
69
|
+
)
|
|
70
|
+
for r in raw_results[:max_results]
|
|
71
|
+
if r.get("url")
|
|
72
|
+
)
|
|
73
|
+
return results
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""SwarmCreateTool — spawn a new swarm worker agent."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
|
|
8
|
+
from llm_code.swarm.manager import SwarmManager
|
|
9
|
+
from llm_code.tools.base import PermissionLevel, Tool, ToolResult
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SwarmCreateInput(BaseModel):
|
|
13
|
+
role: str
|
|
14
|
+
task: str
|
|
15
|
+
backend: str = "auto"
|
|
16
|
+
model: str | None = None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SwarmCreateTool(Tool):
|
|
20
|
+
def __init__(self, manager: SwarmManager) -> None:
|
|
21
|
+
self._manager = manager
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def name(self) -> str:
|
|
25
|
+
return "swarm_create"
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def description(self) -> str:
|
|
29
|
+
return (
|
|
30
|
+
"Spawn a new swarm worker agent with a given role and task. "
|
|
31
|
+
"The worker runs as a separate llm-code --lite process. "
|
|
32
|
+
"Backend: 'auto' (tmux if available, else subprocess), 'tmux', or 'subprocess'."
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def input_schema(self) -> dict:
|
|
37
|
+
return {
|
|
38
|
+
"type": "object",
|
|
39
|
+
"properties": {
|
|
40
|
+
"role": {
|
|
41
|
+
"type": "string",
|
|
42
|
+
"description": "Role of the worker (e.g. 'security reviewer', 'test writer')",
|
|
43
|
+
},
|
|
44
|
+
"task": {
|
|
45
|
+
"type": "string",
|
|
46
|
+
"description": "Task description for the worker to execute",
|
|
47
|
+
},
|
|
48
|
+
"backend": {
|
|
49
|
+
"type": "string",
|
|
50
|
+
"enum": ["auto", "tmux", "subprocess"],
|
|
51
|
+
"description": "Backend to use (default: auto)",
|
|
52
|
+
"default": "auto",
|
|
53
|
+
},
|
|
54
|
+
"model": {
|
|
55
|
+
"type": "string",
|
|
56
|
+
"description": (
|
|
57
|
+
"Override the LLM model for this specific swarm member. "
|
|
58
|
+
"When omitted, the model is resolved via the config fallback chain: "
|
|
59
|
+
"role_models -> model_routing.sub_agent -> global model."
|
|
60
|
+
),
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
"required": ["role", "task"],
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def required_permission(self) -> PermissionLevel:
|
|
68
|
+
return PermissionLevel.FULL_ACCESS
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def input_model(self) -> type[SwarmCreateInput]:
|
|
72
|
+
return SwarmCreateInput
|
|
73
|
+
|
|
74
|
+
def execute(self, args: dict) -> ToolResult:
|
|
75
|
+
role = args["role"]
|
|
76
|
+
task = args["task"]
|
|
77
|
+
backend = args.get("backend", "auto")
|
|
78
|
+
model = args.get("model")
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
try:
|
|
82
|
+
asyncio.get_running_loop()
|
|
83
|
+
running = True
|
|
84
|
+
except RuntimeError:
|
|
85
|
+
running = False
|
|
86
|
+
|
|
87
|
+
if running:
|
|
88
|
+
import concurrent.futures
|
|
89
|
+
with concurrent.futures.ThreadPoolExecutor() as pool:
|
|
90
|
+
member = pool.submit(
|
|
91
|
+
asyncio.run,
|
|
92
|
+
self._manager.create_member(role=role, task=task, backend=backend, model=model),
|
|
93
|
+
).result()
|
|
94
|
+
else:
|
|
95
|
+
member = asyncio.run(
|
|
96
|
+
self._manager.create_member(role=role, task=task, backend=backend, model=model)
|
|
97
|
+
)
|
|
98
|
+
except ValueError as exc:
|
|
99
|
+
return ToolResult(output=str(exc), is_error=True)
|
|
100
|
+
|
|
101
|
+
return ToolResult(
|
|
102
|
+
output=(
|
|
103
|
+
f"Created swarm member {member.id}\n"
|
|
104
|
+
f" Role: {member.role}\n"
|
|
105
|
+
f" Task: {member.task}\n"
|
|
106
|
+
f" Backend: {member.backend}\n"
|
|
107
|
+
f" PID: {member.pid}"
|
|
108
|
+
)
|
|
109
|
+
)
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""SwarmDeleteTool — stop one or all swarm members."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
|
|
8
|
+
from llm_code.swarm.manager import SwarmManager
|
|
9
|
+
from llm_code.tools.base import PermissionLevel, Tool, ToolResult
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SwarmDeleteInput(BaseModel):
|
|
13
|
+
action: str = "stop" # "stop" | "stop_all"
|
|
14
|
+
member_id: str = ""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SwarmDeleteTool(Tool):
|
|
18
|
+
def __init__(self, manager: SwarmManager) -> None:
|
|
19
|
+
self._manager = manager
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def name(self) -> str:
|
|
23
|
+
return "swarm_delete"
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def description(self) -> str:
|
|
27
|
+
return (
|
|
28
|
+
"Stop one or all swarm members. "
|
|
29
|
+
"action='stop' + member_id to stop a single member. "
|
|
30
|
+
"action='stop_all' to stop all members."
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def input_schema(self) -> dict:
|
|
35
|
+
return {
|
|
36
|
+
"type": "object",
|
|
37
|
+
"properties": {
|
|
38
|
+
"action": {
|
|
39
|
+
"type": "string",
|
|
40
|
+
"enum": ["stop", "stop_all"],
|
|
41
|
+
"description": "stop (one) or stop_all",
|
|
42
|
+
"default": "stop",
|
|
43
|
+
},
|
|
44
|
+
"member_id": {
|
|
45
|
+
"type": "string",
|
|
46
|
+
"description": "ID of the member to stop (for action=stop)",
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
"required": ["action"],
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def required_permission(self) -> PermissionLevel:
|
|
54
|
+
return PermissionLevel.FULL_ACCESS
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def input_model(self) -> type[SwarmDeleteInput]:
|
|
58
|
+
return SwarmDeleteInput
|
|
59
|
+
|
|
60
|
+
def execute(self, args: dict) -> ToolResult:
|
|
61
|
+
action = args.get("action", "stop")
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
running = False
|
|
65
|
+
try:
|
|
66
|
+
asyncio.get_running_loop()
|
|
67
|
+
running = True
|
|
68
|
+
except RuntimeError:
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
if action == "stop_all":
|
|
72
|
+
if running:
|
|
73
|
+
import concurrent.futures
|
|
74
|
+
with concurrent.futures.ThreadPoolExecutor() as pool:
|
|
75
|
+
pool.submit(asyncio.run, self._manager.stop_all()).result()
|
|
76
|
+
else:
|
|
77
|
+
asyncio.run(self._manager.stop_all())
|
|
78
|
+
return ToolResult(output="All swarm members stopped.")
|
|
79
|
+
|
|
80
|
+
member_id = args.get("member_id", "")
|
|
81
|
+
if not member_id:
|
|
82
|
+
return ToolResult(output="member_id is required for action=stop", is_error=True)
|
|
83
|
+
|
|
84
|
+
if running:
|
|
85
|
+
import concurrent.futures
|
|
86
|
+
with concurrent.futures.ThreadPoolExecutor() as pool:
|
|
87
|
+
pool.submit(asyncio.run, self._manager.stop_member(member_id)).result()
|
|
88
|
+
else:
|
|
89
|
+
asyncio.run(self._manager.stop_member(member_id))
|
|
90
|
+
return ToolResult(output=f"Stopped swarm member {member_id}")
|
|
91
|
+
|
|
92
|
+
except KeyError:
|
|
93
|
+
return ToolResult(output=f"No swarm member with id '{args.get('member_id')}'", is_error=True)
|
|
94
|
+
except Exception as exc:
|
|
95
|
+
return ToolResult(output=f"Error: {exc}", is_error=True)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""SwarmListTool — list all active swarm members."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from llm_code.swarm.manager import SwarmManager
|
|
5
|
+
from llm_code.tools.base import PermissionLevel, Tool, ToolResult
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SwarmListTool(Tool):
|
|
9
|
+
def __init__(self, manager: SwarmManager) -> None:
|
|
10
|
+
self._manager = manager
|
|
11
|
+
|
|
12
|
+
@property
|
|
13
|
+
def name(self) -> str:
|
|
14
|
+
return "swarm_list"
|
|
15
|
+
|
|
16
|
+
@property
|
|
17
|
+
def description(self) -> str:
|
|
18
|
+
return "List all active swarm worker agents with their roles, tasks, and 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
|
+
members = self._manager.list_members()
|
|
36
|
+
if not members:
|
|
37
|
+
return ToolResult(output="No swarm members active.")
|
|
38
|
+
lines = []
|
|
39
|
+
for m in members:
|
|
40
|
+
lines.append(
|
|
41
|
+
f"- {m.id} | role={m.role} | task={m.task[:50]} | "
|
|
42
|
+
f"backend={m.backend} | pid={m.pid} | status={m.status.value}"
|
|
43
|
+
)
|
|
44
|
+
return ToolResult(output="\n".join(lines))
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""SwarmMessageTool — send and receive messages between swarm members."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
|
|
6
|
+
from llm_code.swarm.manager import SwarmManager
|
|
7
|
+
from llm_code.tools.base import PermissionLevel, Tool, ToolResult
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SwarmMessageInput(BaseModel):
|
|
11
|
+
action: str # "send" | "receive" | "broadcast" | "pending"
|
|
12
|
+
from_id: str = ""
|
|
13
|
+
to_id: str = ""
|
|
14
|
+
text: str = ""
|
|
15
|
+
member_ids: list[str] = []
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SwarmMessageTool(Tool):
|
|
19
|
+
def __init__(self, manager: SwarmManager) -> None:
|
|
20
|
+
self._manager = manager
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def name(self) -> str:
|
|
24
|
+
return "swarm_message"
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def description(self) -> str:
|
|
28
|
+
return (
|
|
29
|
+
"Send and receive messages between swarm members. "
|
|
30
|
+
"Actions: 'send' (from_id, to_id, text), "
|
|
31
|
+
"'receive' (from_id, to_id), "
|
|
32
|
+
"'broadcast' (from_id, member_ids, text), "
|
|
33
|
+
"'pending' (to_id — show unread messages for a member)."
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def input_schema(self) -> dict:
|
|
38
|
+
return {
|
|
39
|
+
"type": "object",
|
|
40
|
+
"properties": {
|
|
41
|
+
"action": {
|
|
42
|
+
"type": "string",
|
|
43
|
+
"enum": ["send", "receive", "broadcast", "pending"],
|
|
44
|
+
"description": "Message action to perform",
|
|
45
|
+
},
|
|
46
|
+
"from_id": {"type": "string", "description": "Sender ID"},
|
|
47
|
+
"to_id": {"type": "string", "description": "Receiver ID"},
|
|
48
|
+
"text": {"type": "string", "description": "Message text"},
|
|
49
|
+
"member_ids": {
|
|
50
|
+
"type": "array",
|
|
51
|
+
"items": {"type": "string"},
|
|
52
|
+
"description": "List of member IDs for broadcast",
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
"required": ["action"],
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def required_permission(self) -> PermissionLevel:
|
|
60
|
+
return PermissionLevel.READ_ONLY
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def input_model(self) -> type[SwarmMessageInput]:
|
|
64
|
+
return SwarmMessageInput
|
|
65
|
+
|
|
66
|
+
def execute(self, args: dict) -> ToolResult:
|
|
67
|
+
action = args["action"]
|
|
68
|
+
mailbox = self._manager.mailbox
|
|
69
|
+
|
|
70
|
+
if action == "send":
|
|
71
|
+
from_id = args.get("from_id", "main")
|
|
72
|
+
to_id = args.get("to_id", "")
|
|
73
|
+
text = args.get("text", "")
|
|
74
|
+
if not to_id or not text:
|
|
75
|
+
return ToolResult(output="send requires to_id and text", is_error=True)
|
|
76
|
+
mailbox.send(from_id, to_id, text)
|
|
77
|
+
return ToolResult(output=f"Message sent from {from_id} to {to_id}")
|
|
78
|
+
|
|
79
|
+
if action == "receive":
|
|
80
|
+
from_id = args.get("from_id", "")
|
|
81
|
+
to_id = args.get("to_id", "")
|
|
82
|
+
if not from_id or not to_id:
|
|
83
|
+
return ToolResult(output="receive requires from_id and to_id", is_error=True)
|
|
84
|
+
msgs = mailbox.receive(from_id, to_id)
|
|
85
|
+
if not msgs:
|
|
86
|
+
return ToolResult(output="No messages.")
|
|
87
|
+
lines = [f"[{m.timestamp}] {m.from_id} -> {m.to_id}: {m.text}" for m in msgs]
|
|
88
|
+
return ToolResult(output="\n".join(lines))
|
|
89
|
+
|
|
90
|
+
if action == "broadcast":
|
|
91
|
+
from_id = args.get("from_id", "main")
|
|
92
|
+
member_ids = args.get("member_ids", [])
|
|
93
|
+
text = args.get("text", "")
|
|
94
|
+
if not member_ids or not text:
|
|
95
|
+
return ToolResult(output="broadcast requires member_ids and text", is_error=True)
|
|
96
|
+
mailbox.broadcast(from_id, member_ids, text)
|
|
97
|
+
return ToolResult(output=f"Broadcast sent to {len(member_ids)} members")
|
|
98
|
+
|
|
99
|
+
if action == "pending":
|
|
100
|
+
to_id = args.get("to_id", "")
|
|
101
|
+
if not to_id:
|
|
102
|
+
return ToolResult(output="pending requires to_id", is_error=True)
|
|
103
|
+
msgs = mailbox.pending_for(to_id)
|
|
104
|
+
if not msgs:
|
|
105
|
+
return ToolResult(output=f"No pending messages for {to_id}")
|
|
106
|
+
lines = [f"[{m.timestamp}] {m.from_id}: {m.text}" for m in msgs]
|
|
107
|
+
return ToolResult(output="\n".join(lines))
|
|
108
|
+
|
|
109
|
+
return ToolResult(output=f"Unknown action: {action}", is_error=True)
|