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.
Files changed (212) hide show
  1. llm_code/__init__.py +2 -0
  2. llm_code/analysis/__init__.py +6 -0
  3. llm_code/analysis/cache.py +33 -0
  4. llm_code/analysis/engine.py +256 -0
  5. llm_code/analysis/go_rules.py +114 -0
  6. llm_code/analysis/js_rules.py +84 -0
  7. llm_code/analysis/python_rules.py +311 -0
  8. llm_code/analysis/rules.py +140 -0
  9. llm_code/analysis/rust_rules.py +108 -0
  10. llm_code/analysis/universal_rules.py +111 -0
  11. llm_code/api/__init__.py +0 -0
  12. llm_code/api/client.py +90 -0
  13. llm_code/api/errors.py +73 -0
  14. llm_code/api/openai_compat.py +390 -0
  15. llm_code/api/provider.py +35 -0
  16. llm_code/api/sse.py +52 -0
  17. llm_code/api/types.py +140 -0
  18. llm_code/cli/__init__.py +0 -0
  19. llm_code/cli/commands.py +70 -0
  20. llm_code/cli/image.py +122 -0
  21. llm_code/cli/render.py +214 -0
  22. llm_code/cli/status_line.py +79 -0
  23. llm_code/cli/streaming.py +92 -0
  24. llm_code/cli/tui_main.py +220 -0
  25. llm_code/computer_use/__init__.py +11 -0
  26. llm_code/computer_use/app_detect.py +49 -0
  27. llm_code/computer_use/app_tier.py +57 -0
  28. llm_code/computer_use/coordinator.py +99 -0
  29. llm_code/computer_use/input_control.py +71 -0
  30. llm_code/computer_use/screenshot.py +93 -0
  31. llm_code/cron/__init__.py +13 -0
  32. llm_code/cron/parser.py +145 -0
  33. llm_code/cron/scheduler.py +135 -0
  34. llm_code/cron/storage.py +126 -0
  35. llm_code/enterprise/__init__.py +1 -0
  36. llm_code/enterprise/audit.py +59 -0
  37. llm_code/enterprise/auth.py +26 -0
  38. llm_code/enterprise/oidc.py +95 -0
  39. llm_code/enterprise/rbac.py +65 -0
  40. llm_code/harness/__init__.py +5 -0
  41. llm_code/harness/config.py +33 -0
  42. llm_code/harness/engine.py +129 -0
  43. llm_code/harness/guides.py +41 -0
  44. llm_code/harness/sensors.py +68 -0
  45. llm_code/harness/templates.py +84 -0
  46. llm_code/hida/__init__.py +1 -0
  47. llm_code/hida/classifier.py +187 -0
  48. llm_code/hida/engine.py +49 -0
  49. llm_code/hida/profiles.py +95 -0
  50. llm_code/hida/types.py +28 -0
  51. llm_code/ide/__init__.py +1 -0
  52. llm_code/ide/bridge.py +80 -0
  53. llm_code/ide/detector.py +76 -0
  54. llm_code/ide/server.py +169 -0
  55. llm_code/logging.py +29 -0
  56. llm_code/lsp/__init__.py +0 -0
  57. llm_code/lsp/client.py +298 -0
  58. llm_code/lsp/detector.py +42 -0
  59. llm_code/lsp/manager.py +56 -0
  60. llm_code/lsp/tools.py +288 -0
  61. llm_code/marketplace/__init__.py +0 -0
  62. llm_code/marketplace/builtin_registry.py +102 -0
  63. llm_code/marketplace/installer.py +162 -0
  64. llm_code/marketplace/plugin.py +78 -0
  65. llm_code/marketplace/registry.py +360 -0
  66. llm_code/mcp/__init__.py +0 -0
  67. llm_code/mcp/bridge.py +87 -0
  68. llm_code/mcp/client.py +117 -0
  69. llm_code/mcp/health.py +120 -0
  70. llm_code/mcp/manager.py +214 -0
  71. llm_code/mcp/oauth.py +219 -0
  72. llm_code/mcp/transport.py +254 -0
  73. llm_code/mcp/types.py +53 -0
  74. llm_code/remote/__init__.py +0 -0
  75. llm_code/remote/client.py +136 -0
  76. llm_code/remote/protocol.py +22 -0
  77. llm_code/remote/server.py +275 -0
  78. llm_code/remote/ssh_proxy.py +56 -0
  79. llm_code/runtime/__init__.py +0 -0
  80. llm_code/runtime/auto_commit.py +56 -0
  81. llm_code/runtime/auto_diagnose.py +62 -0
  82. llm_code/runtime/checkpoint.py +70 -0
  83. llm_code/runtime/checkpoint_recovery.py +142 -0
  84. llm_code/runtime/compaction.py +35 -0
  85. llm_code/runtime/compressor.py +415 -0
  86. llm_code/runtime/config.py +533 -0
  87. llm_code/runtime/context.py +49 -0
  88. llm_code/runtime/conversation.py +921 -0
  89. llm_code/runtime/cost_tracker.py +126 -0
  90. llm_code/runtime/dream.py +127 -0
  91. llm_code/runtime/file_protection.py +150 -0
  92. llm_code/runtime/hardware.py +85 -0
  93. llm_code/runtime/hooks.py +223 -0
  94. llm_code/runtime/indexer.py +230 -0
  95. llm_code/runtime/knowledge_compiler.py +232 -0
  96. llm_code/runtime/memory.py +132 -0
  97. llm_code/runtime/memory_layers.py +467 -0
  98. llm_code/runtime/memory_lint.py +252 -0
  99. llm_code/runtime/model_aliases.py +37 -0
  100. llm_code/runtime/ollama.py +93 -0
  101. llm_code/runtime/overlay.py +124 -0
  102. llm_code/runtime/permissions.py +200 -0
  103. llm_code/runtime/plan.py +45 -0
  104. llm_code/runtime/prompt.py +238 -0
  105. llm_code/runtime/repo_map.py +174 -0
  106. llm_code/runtime/sandbox.py +116 -0
  107. llm_code/runtime/session.py +268 -0
  108. llm_code/runtime/skill_resolver.py +61 -0
  109. llm_code/runtime/skills.py +133 -0
  110. llm_code/runtime/speculative.py +75 -0
  111. llm_code/runtime/streaming_executor.py +216 -0
  112. llm_code/runtime/telemetry.py +196 -0
  113. llm_code/runtime/token_budget.py +26 -0
  114. llm_code/runtime/vcr.py +142 -0
  115. llm_code/runtime/vision.py +102 -0
  116. llm_code/swarm/__init__.py +1 -0
  117. llm_code/swarm/backend_subprocess.py +108 -0
  118. llm_code/swarm/backend_tmux.py +103 -0
  119. llm_code/swarm/backend_worktree.py +306 -0
  120. llm_code/swarm/checkpoint.py +74 -0
  121. llm_code/swarm/coordinator.py +236 -0
  122. llm_code/swarm/mailbox.py +88 -0
  123. llm_code/swarm/manager.py +202 -0
  124. llm_code/swarm/memory_sync.py +80 -0
  125. llm_code/swarm/recovery.py +21 -0
  126. llm_code/swarm/team.py +67 -0
  127. llm_code/swarm/types.py +31 -0
  128. llm_code/task/__init__.py +16 -0
  129. llm_code/task/diagnostics.py +93 -0
  130. llm_code/task/manager.py +162 -0
  131. llm_code/task/types.py +112 -0
  132. llm_code/task/verifier.py +104 -0
  133. llm_code/tools/__init__.py +0 -0
  134. llm_code/tools/agent.py +145 -0
  135. llm_code/tools/agent_roles.py +82 -0
  136. llm_code/tools/base.py +94 -0
  137. llm_code/tools/bash.py +565 -0
  138. llm_code/tools/computer_use_tools.py +278 -0
  139. llm_code/tools/coordinator_tool.py +75 -0
  140. llm_code/tools/cron_create.py +90 -0
  141. llm_code/tools/cron_delete.py +49 -0
  142. llm_code/tools/cron_list.py +51 -0
  143. llm_code/tools/deferred.py +92 -0
  144. llm_code/tools/dump.py +116 -0
  145. llm_code/tools/edit_file.py +282 -0
  146. llm_code/tools/git_tools.py +531 -0
  147. llm_code/tools/glob_search.py +112 -0
  148. llm_code/tools/grep_search.py +144 -0
  149. llm_code/tools/ide_diagnostics.py +59 -0
  150. llm_code/tools/ide_open.py +58 -0
  151. llm_code/tools/ide_selection.py +52 -0
  152. llm_code/tools/memory_tools.py +138 -0
  153. llm_code/tools/multi_edit.py +143 -0
  154. llm_code/tools/notebook_edit.py +107 -0
  155. llm_code/tools/notebook_read.py +81 -0
  156. llm_code/tools/parsing.py +63 -0
  157. llm_code/tools/read_file.py +154 -0
  158. llm_code/tools/registry.py +58 -0
  159. llm_code/tools/search_backends/__init__.py +56 -0
  160. llm_code/tools/search_backends/brave.py +56 -0
  161. llm_code/tools/search_backends/duckduckgo.py +129 -0
  162. llm_code/tools/search_backends/searxng.py +71 -0
  163. llm_code/tools/search_backends/tavily.py +73 -0
  164. llm_code/tools/swarm_create.py +109 -0
  165. llm_code/tools/swarm_delete.py +95 -0
  166. llm_code/tools/swarm_list.py +44 -0
  167. llm_code/tools/swarm_message.py +109 -0
  168. llm_code/tools/task_close.py +79 -0
  169. llm_code/tools/task_plan.py +79 -0
  170. llm_code/tools/task_verify.py +90 -0
  171. llm_code/tools/tool_search.py +65 -0
  172. llm_code/tools/web_common.py +258 -0
  173. llm_code/tools/web_fetch.py +223 -0
  174. llm_code/tools/web_search.py +280 -0
  175. llm_code/tools/write_file.py +118 -0
  176. llm_code/tui/__init__.py +1 -0
  177. llm_code/tui/app.py +2432 -0
  178. llm_code/tui/chat_view.py +82 -0
  179. llm_code/tui/chat_widgets.py +309 -0
  180. llm_code/tui/header_bar.py +46 -0
  181. llm_code/tui/input_bar.py +349 -0
  182. llm_code/tui/keybindings.py +142 -0
  183. llm_code/tui/marketplace.py +210 -0
  184. llm_code/tui/status_bar.py +72 -0
  185. llm_code/tui/theme.py +96 -0
  186. llm_code/utils/__init__.py +0 -0
  187. llm_code/utils/diff.py +111 -0
  188. llm_code/utils/errors.py +70 -0
  189. llm_code/utils/hyperlink.py +73 -0
  190. llm_code/utils/notebook.py +179 -0
  191. llm_code/utils/search.py +69 -0
  192. llm_code/utils/text_normalize.py +28 -0
  193. llm_code/utils/version_check.py +62 -0
  194. llm_code/vim/__init__.py +4 -0
  195. llm_code/vim/engine.py +51 -0
  196. llm_code/vim/motions.py +172 -0
  197. llm_code/vim/operators.py +183 -0
  198. llm_code/vim/text_objects.py +139 -0
  199. llm_code/vim/transitions.py +279 -0
  200. llm_code/vim/types.py +68 -0
  201. llm_code/voice/__init__.py +1 -0
  202. llm_code/voice/languages.py +43 -0
  203. llm_code/voice/recorder.py +136 -0
  204. llm_code/voice/stt.py +36 -0
  205. llm_code/voice/stt_anthropic.py +66 -0
  206. llm_code/voice/stt_google.py +32 -0
  207. llm_code/voice/stt_whisper.py +52 -0
  208. llmcode_cli-1.0.0.dist-info/METADATA +524 -0
  209. llmcode_cli-1.0.0.dist-info/RECORD +212 -0
  210. llmcode_cli-1.0.0.dist-info/WHEEL +4 -0
  211. llmcode_cli-1.0.0.dist-info/entry_points.txt +2 -0
  212. 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)