opencommand 0.1.0__tar.gz

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 (36) hide show
  1. opencommand-0.1.0/PKG-INFO +76 -0
  2. opencommand-0.1.0/README.md +54 -0
  3. opencommand-0.1.0/pyproject.toml +39 -0
  4. opencommand-0.1.0/src/opencommand/__init__.py +4 -0
  5. opencommand-0.1.0/src/opencommand/cli.py +375 -0
  6. opencommand-0.1.0/src/opencommand/core/agent.py +91 -0
  7. opencommand-0.1.0/src/opencommand/core/config.py +65 -0
  8. opencommand-0.1.0/src/opencommand/core/errors.py +23 -0
  9. opencommand-0.1.0/src/opencommand/core/logging.py +65 -0
  10. opencommand-0.1.0/src/opencommand/core/supervisor.py +81 -0
  11. opencommand-0.1.0/src/opencommand/core/tools.py +136 -0
  12. opencommand-0.1.0/src/opencommand/engine/__init__.py +66 -0
  13. opencommand-0.1.0/src/opencommand/engine/runner.py +275 -0
  14. opencommand-0.1.0/src/opencommand/modules/knowledge.py +85 -0
  15. opencommand-0.1.0/src/opencommand/modules/memory/MEMORY.md +14 -0
  16. opencommand-0.1.0/src/opencommand/modules/skills/bash.md +14 -0
  17. opencommand-0.1.0/src/opencommand/modules/skills/powershell.md +15 -0
  18. opencommand-0.1.0/src/opencommand/modules/skills/python.md +20 -0
  19. opencommand-0.1.0/src/opencommand/modules/skills/system.md +38 -0
  20. opencommand-0.1.0/src/opencommand/modules/system/automatic.py +151 -0
  21. opencommand-0.1.0/src/opencommand/modules/system/cron.py +193 -0
  22. opencommand-0.1.0/src/opencommand/modules/system/notify.py +85 -0
  23. opencommand-0.1.0/src/opencommand/modules/system/platform.py +197 -0
  24. opencommand-0.1.0/src/opencommand/modules/templates.py +227 -0
  25. opencommand-0.1.0/src/opencommand/modules/tools/desktop.py +127 -0
  26. opencommand-0.1.0/src/opencommand/modules/tools/files.py +82 -0
  27. opencommand-0.1.0/src/opencommand/modules/tools/instructions.py +163 -0
  28. opencommand-0.1.0/src/opencommand/modules/tools/memory.py +78 -0
  29. opencommand-0.1.0/src/opencommand/modules/tools/playwright.py +61 -0
  30. opencommand-0.1.0/src/opencommand/modules/tools/research.py +46 -0
  31. opencommand-0.1.0/src/opencommand/modules/tools/system.py +163 -0
  32. opencommand-0.1.0/src/opencommand/modules/tools/terminal.py +53 -0
  33. opencommand-0.1.0/src/opencommand/providers/provider.py +157 -0
  34. opencommand-0.1.0/src/opencommand/swarm/agents/commander.py +530 -0
  35. opencommand-0.1.0/src/opencommand/swarm/agents/worker.py +26 -0
  36. opencommand-0.1.0/src/opencommand/tui/dashboard.py +41 -0
@@ -0,0 +1,76 @@
1
+ Metadata-Version: 2.3
2
+ Name: opencommand
3
+ Version: 0.1.0
4
+ Summary: Local-first swarm agent framework: a Commander plans and dispatches a swarm of Workers that research, code, test, and interactively drive the computer to complete long-horizon codebase goals.
5
+ Author: DSiENT
6
+ Author-email: DSiENT <w.caskey7@gmail.com>
7
+ Requires-Dist: asciimatics>=1.15.0
8
+ Requires-Dist: click>=8.4.2
9
+ Requires-Dist: playwright>=1.61.0
10
+ Requires-Dist: pyautogui>=0.9.54
11
+ Requires-Dist: pynput>=1.8.2
12
+ Requires-Dist: requests>=2.34.2
13
+ Requires-Dist: mss>=9.0.0
14
+ Requires-Dist: pillow>=11.0.0
15
+ Requires-Dist: llama-cpp-python>=0.3.33
16
+ Requires-Dist: watchdog>=6.0.0
17
+ Requires-Dist: ntfy>=2.7.1
18
+ Requires-Python: >=3.14
19
+ Project-URL: Homepage, https://github.com/D5-Interactive/OpenCommand
20
+ Project-URL: Repository, https://github.com/D5-Interactive/OpenCommand
21
+ Description-Content-Type: text/markdown
22
+
23
+ # OpenCommand
24
+
25
+ Local-first swarm agent framework. A **Commander** model plans and dispatches a
26
+ swarm of **Workers** that research, code, test, and interactively drive the
27
+ computer (mouse, keyboard, screen) to complete long-horizon codebase goals.
28
+
29
+ Everything runs **embedded** — models are GGUF files loaded directly with
30
+ `llama-cpp-python` (no API servers, no network at runtime). Designed for
31
+ **Python 3.14** (free-threaded).
32
+
33
+ ## Install
34
+
35
+ ```bash
36
+ # Run without installing (ephemeral):
37
+ uv tool run opencommand --help
38
+
39
+ # Or install globally:
40
+ uv tool install opencommand
41
+ playwright install # optional, for the playwright tool
42
+ ```
43
+
44
+ > **Note:** the `llama-cpp-python` prebuilt CPU wheel may crash on older CPUs
45
+ > (illegal-instruction). If so, rebuild from source with AVX2:
46
+ > ```bat
47
+ > call "C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\VC\Auxiliary\Build\vcvars64.bat"
48
+ > set CMAKE_ARGS=-DGGML_AVX2=ON
49
+ > uv pip install --no-binary llama-cpp-python --no-cache llama-cpp-python
50
+ > ```
51
+
52
+ ## Usage
53
+
54
+ ```bash
55
+ opencommand models list # list built-in embedded models
56
+ opencommand models pull all # download GGUF models into ./models
57
+ opencommand run "Add unit tests for the engine module."
58
+ opencommand run "Build a Panda3D FPS" --pipeline advanced --verify "uv run pytest -q"
59
+ opencommand tui # live swarm dashboard
60
+ opencommand cron add healthcheck "every 1h" "echo ok"
61
+ ```
62
+
63
+ ## Built-in models
64
+
65
+ | Role | Model |
66
+ |---------------|----------------------------------------|
67
+ | commander | `deepreinforce-ai/Ornith-1.0-9B-GGUF` |
68
+ | worker | `unsloth/Qwen3-4B-Instruct-2507-GGUF` |
69
+ | vision | `unsloth/Qwen3-VL-4B-Instruct-GGUF` |
70
+ | vision_small | `openbmb/MiniCPM-V-4.6-gguf` |
71
+
72
+ ## Links
73
+
74
+ - `DESIGN.md` — full architecture & research notes
75
+ - `TODO.md` — roadmap tracker
76
+
@@ -0,0 +1,54 @@
1
+ # OpenCommand
2
+
3
+ Local-first swarm agent framework. A **Commander** model plans and dispatches a
4
+ swarm of **Workers** that research, code, test, and interactively drive the
5
+ computer (mouse, keyboard, screen) to complete long-horizon codebase goals.
6
+
7
+ Everything runs **embedded** — models are GGUF files loaded directly with
8
+ `llama-cpp-python` (no API servers, no network at runtime). Designed for
9
+ **Python 3.14** (free-threaded).
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ # Run without installing (ephemeral):
15
+ uv tool run opencommand --help
16
+
17
+ # Or install globally:
18
+ uv tool install opencommand
19
+ playwright install # optional, for the playwright tool
20
+ ```
21
+
22
+ > **Note:** the `llama-cpp-python` prebuilt CPU wheel may crash on older CPUs
23
+ > (illegal-instruction). If so, rebuild from source with AVX2:
24
+ > ```bat
25
+ > call "C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\VC\Auxiliary\Build\vcvars64.bat"
26
+ > set CMAKE_ARGS=-DGGML_AVX2=ON
27
+ > uv pip install --no-binary llama-cpp-python --no-cache llama-cpp-python
28
+ > ```
29
+
30
+ ## Usage
31
+
32
+ ```bash
33
+ opencommand models list # list built-in embedded models
34
+ opencommand models pull all # download GGUF models into ./models
35
+ opencommand run "Add unit tests for the engine module."
36
+ opencommand run "Build a Panda3D FPS" --pipeline advanced --verify "uv run pytest -q"
37
+ opencommand tui # live swarm dashboard
38
+ opencommand cron add healthcheck "every 1h" "echo ok"
39
+ ```
40
+
41
+ ## Built-in models
42
+
43
+ | Role | Model |
44
+ |---------------|----------------------------------------|
45
+ | commander | `deepreinforce-ai/Ornith-1.0-9B-GGUF` |
46
+ | worker | `unsloth/Qwen3-4B-Instruct-2507-GGUF` |
47
+ | vision | `unsloth/Qwen3-VL-4B-Instruct-GGUF` |
48
+ | vision_small | `openbmb/MiniCPM-V-4.6-gguf` |
49
+
50
+ ## Links
51
+
52
+ - `DESIGN.md` — full architecture & research notes
53
+ - `TODO.md` — roadmap tracker
54
+
@@ -0,0 +1,39 @@
1
+ [project]
2
+ name = "opencommand"
3
+ version = "0.1.0"
4
+ description = "Local-first swarm agent framework: a Commander plans and dispatches a swarm of Workers that research, code, test, and interactively drive the computer to complete long-horizon codebase goals."
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "DSiENT", email = "w.caskey7@gmail.com" }
8
+ ]
9
+ requires-python = ">=3.14"
10
+ dependencies = [
11
+ "asciimatics>=1.15.0",
12
+ "click>=8.4.2",
13
+ "playwright>=1.61.0",
14
+ "pyautogui>=0.9.54",
15
+ "pynput>=1.8.2",
16
+ "requests>=2.34.2",
17
+ "mss>=9.0.0",
18
+ "pillow>=11.0.0",
19
+ "llama-cpp-python>=0.3.33",
20
+ "watchdog>=6.0.0",
21
+ "ntfy>=2.7.1",
22
+ ]
23
+
24
+ [project.scripts]
25
+ opencommand = "opencommand:main"
26
+
27
+ [project.urls]
28
+ Homepage = "https://github.com/D5-Interactive/OpenCommand"
29
+ Repository = "https://github.com/D5-Interactive/OpenCommand"
30
+
31
+ [dependency-groups]
32
+ dev = ["pytest>=8.0"]
33
+
34
+ [tool.uv]
35
+ package = true
36
+
37
+ [build-system]
38
+ requires = ["uv_build>=0.11.19,<0.12.0"]
39
+ build-backend = "uv_build"
@@ -0,0 +1,4 @@
1
+ def main() -> None:
2
+ from .cli import cli
3
+
4
+ cli()
@@ -0,0 +1,375 @@
1
+ """click-based CLI for OpenCommand (fully embedded, no external servers).
2
+
3
+ Commands:
4
+ opencommand workspace [path] # show workspace + engine status
5
+ opencommand models pull [role] # download built-in GGUF models
6
+ opencommand models list # list built-in model catalog
7
+ opencommand run "goal" # run the swarm autonomously
8
+ opencommand tui # live swarm dashboard
9
+ opencommand notify "msg" # send a test ntfy notification
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import logging
15
+ import os
16
+ import sys
17
+ from pathlib import Path
18
+
19
+ import click
20
+
21
+ from .core.config import OpenCommandConfig
22
+ from .core.logging import configure_logging, get_logger
23
+ from .core.supervisor import Supervisor
24
+ from .core.tools import ToolRegistry
25
+ from .engine import Engine
26
+ from .modules.system.platform import describe
27
+
28
+ log = get_logger("cli")
29
+
30
+
31
+ def _build_tools(engine: Engine, cfg: OpenCommandConfig) -> ToolRegistry:
32
+ from .modules.tools.desktop import DesktopTool
33
+ from .modules.tools.files import FilesTool
34
+ from .modules.tools.instructions import InstructionsTool
35
+ from .modules.tools.memory import MemoryTool
36
+ from .modules.tools.playwright import PlaywrightTool
37
+ from .modules.tools.research import ResearchTool
38
+ from .modules.tools.system import SystemTool
39
+ from .modules.tools.terminal import TerminalTool
40
+
41
+ reg = ToolRegistry()
42
+ reg.register(TerminalTool())
43
+ reg.register(FilesTool(cfg.workspace))
44
+ reg.register(MemoryTool(cfg.workspace))
45
+ reg.register(InstructionsTool(cfg.workspace))
46
+ reg.register(ResearchTool())
47
+ reg.register(PlaywrightTool())
48
+ reg.register(SystemTool())
49
+ vision_role = cfg.vision_model if cfg.vision else None
50
+ reg.register(DesktopTool(engine.runner(vision_role) if vision_role else None))
51
+ return reg
52
+
53
+
54
+ def _load_cfg(path: str | None) -> OpenCommandConfig:
55
+ ws = Path(path).resolve() if path else Path.cwd()
56
+ return OpenCommandConfig.load(ws)
57
+
58
+
59
+ @click.group()
60
+ @click.version_option(version="0.1.0")
61
+ @click.option("-v", "--verbose", is_flag=True, help="Enable verbose (DEBUG) logging.")
62
+ def cli(verbose: bool) -> None:
63
+ """OpenCommand - all-in-one local model runner & command center."""
64
+ configure_logging(level=logging.DEBUG if verbose else logging.INFO,
65
+ workspace=_load_cfg(None).workspace, verbose=verbose)
66
+
67
+
68
+ @cli.command()
69
+ @click.argument("path", required=False)
70
+ def workspace(path: str | None) -> None:
71
+ """Show workspace info and embedded engine status."""
72
+ cfg = _load_cfg(path)
73
+ engine = Engine(model_dir=cfg.workspace / "models")
74
+ click.echo(f"Workspace: {cfg.workspace}")
75
+ click.echo(f"Platform: {describe()}")
76
+ click.echo(engine.status())
77
+
78
+
79
+ @cli.command()
80
+ @click.argument("path", required=False)
81
+ @click.option("--apply", is_flag=True, help="Write the recommended config to the workspace.")
82
+ @click.option("--rebuild", is_flag=True, help="Rebuild llama-cpp-python from source with detected ISA flags.")
83
+ def setup(path: str | None, apply: bool, rebuild: bool) -> None:
84
+ """Autodetect the host and show (or --apply / --rebuild) a per-host setup."""
85
+ from .modules.system.platform import detect, recommend_config
86
+
87
+ ws = Path(path).resolve() if path else Path.cwd()
88
+ d = detect()
89
+ rec = recommend_config(d)
90
+ click.echo(describe())
91
+ click.echo(f"Cores: {d['cores']} RAM: {d['ram_gb']} GB GPU: {d['gpu']['backend']}")
92
+ click.echo(f"ISA: {', '.join(d['isa']['extensions']) or 'base'}")
93
+ click.echo(f"Tools: {', '.join(k for k, v in d['tools'].items() if v) or 'none'}")
94
+ click.echo("\nRecommended config:")
95
+ click.echo(f" max_workers={rec['config']['max_workers']} "
96
+ f"pool_size={rec['engine']['pool_size']} "
97
+ f"n_gpu_layers={rec['runner']['n_gpu_layers']}")
98
+ click.echo(f" vision={rec['config']['vision']}")
99
+ if rec["build"]["cmake_args"]:
100
+ click.echo(f" llama.cpp build: {rec['build']['cmake_args']}")
101
+ if apply:
102
+ cfg = OpenCommandConfig.from_host(ws)
103
+ cfg.save()
104
+ click.echo(click.style(f"\nWrote config -> {cfg._path()}", fg="green"))
105
+ if rebuild:
106
+ _rebuild_llama_cpp(rec["build"]["cmake_args"])
107
+
108
+
109
+ def _rebuild_llama_cpp(cmake_args: str) -> None:
110
+ """Reinstall llama-cpp-python from source with the given CMAKE_ARGS.
111
+
112
+ Mirrors the manual x64 AVX2 build: on Windows we must compile inside the
113
+ MSVC x64 dev shell (vcvars64.bat), otherwise a 32-bit DLL is produced.
114
+ """
115
+ import subprocess
116
+
117
+ click.echo(click.style(f"\nRebuilding llama-cpp-python (CMAKE_ARGS={cmake_args or 'none'})...",
118
+ fg="yellow"))
119
+ if not cmake_args:
120
+ click.echo("No ISA build flags detected; building with defaults.")
121
+ cmd, shell, env = _build_rebuild_command(cmake_args)
122
+ try:
123
+ subprocess.run(cmd, shell=shell, env=env, check=True)
124
+ click.echo(click.style("llama-cpp-python rebuilt.", fg="green"))
125
+ except subprocess.CalledProcessError as e:
126
+ click.echo(click.style(f"Rebuild failed (exit {e.returncode}). "
127
+ "Ensure MSVC Build Tools 2022 is installed.", fg="red"))
128
+
129
+
130
+ def _build_rebuild_command(cmake_args: str) -> tuple:
131
+ """Return (command, shell, env) for the llama-cpp-python source rebuild.
132
+
133
+ Windows compiles inside the MSVC x64 dev shell; POSIX sets CMAKE_ARGS in env.
134
+ """
135
+ base = ["uv", "pip", "install", "--no-binary", "llama-cpp-python",
136
+ "--no-cache", "llama-cpp-python"]
137
+ if sys.platform.startswith("win"):
138
+ vcvars = (r"C:\Program Files\Microsoft Visual Studio\2022\BuildTools"
139
+ r"\Common7\Tools\vcvars64.bat")
140
+ if not Path(vcvars).exists():
141
+ vcvars = (r"C:\Program Files (x86)\Microsoft Visual Studio\2022"
142
+ r"\BuildTools\Common7\Tools\vcvars64.bat")
143
+ cmd = f'call "{vcvars}" && set CMAKE_ARGS={cmake_args} && ' + " ".join(base)
144
+ return cmd, True, None
145
+ return base, False, dict(os.environ, CMAKE_ARGS=cmake_args)
146
+
147
+
148
+ @cli.group()
149
+ def models() -> None:
150
+ """Manage built-in embedded models."""
151
+
152
+
153
+ @models.command("list")
154
+ def models_list() -> None:
155
+ """List the built-in model catalog."""
156
+ from .engine.runner import CATALOG
157
+
158
+ for spec in CATALOG.values():
159
+ click.echo(f" {spec.role:10} {spec.repo_id} [{spec.kind}] fmt={spec.chat_format}")
160
+
161
+
162
+ @models.command("pull")
163
+ @click.argument("role", required=False)
164
+ @click.option("--path", default=None, help="Workspace directory.")
165
+ def models_pull(role: str | None, path: str | None) -> None:
166
+ """Download built-in GGUF model(s). Role: commander|worker|vision|all."""
167
+ cfg = _load_cfg(path)
168
+ engine = Engine(model_dir=cfg.workspace / "models")
169
+ roles = ["commander", "worker", "vision", "vision_small"] if role in (None, "all") else [role]
170
+ for r in roles:
171
+ click.echo(f"Downloading {r}...")
172
+ p = engine.ensure_downloaded(r)
173
+ click.echo(f" -> {p}")
174
+
175
+
176
+ @cli.group()
177
+ def cron() -> None:
178
+ """Schedule recurring tasks (e.g. 'every 1h', 'daily 09:00')."""
179
+
180
+
181
+ @cron.command("add")
182
+ @click.argument("name")
183
+ @click.argument("schedule")
184
+ @click.argument("command")
185
+ @click.option("--path", default=None, help="Workspace directory.")
186
+ def cron_add(name: str, schedule: str, command: str, path: str | None) -> None:
187
+ """Add a cron job NAME on SCHEDULE that runs COMMAND (shell)."""
188
+ from .modules.system.cron import CronScheduler
189
+
190
+ cfg = _load_cfg(path)
191
+ cron_file = cfg.workspace / ".opencommand" / "cron.json"
192
+ sched = CronScheduler.load(cron_file)
193
+ sched.schedule_command(name, schedule, command)
194
+ sched.save(cron_file)
195
+ click.echo(f"Added cron job '{name}' on '{schedule}': {command}")
196
+ click.echo(f"Persisted to {cron_file}")
197
+
198
+
199
+ @cron.command("list")
200
+ @click.option("--path", default=None, help="Workspace directory.")
201
+ def cron_list(path: str | None) -> None:
202
+ """List persisted cron jobs."""
203
+ from .modules.system.cron import CronScheduler
204
+
205
+ cfg = _load_cfg(path)
206
+ cron_file = cfg.workspace / ".opencommand" / "cron.json"
207
+ sched = CronScheduler.load(cron_file)
208
+ click.echo(sched.status())
209
+
210
+
211
+ @cron.command("remove")
212
+ @click.argument("name")
213
+ @click.option("--path", default=None, help="Workspace directory.")
214
+ def cron_remove(name: str, path: str | None) -> None:
215
+ """Remove a persisted cron job by NAME."""
216
+ import json as _json
217
+
218
+ cfg = _load_cfg(path)
219
+ cron_file = cfg.workspace / ".opencommand" / "cron.json"
220
+ if not cron_file.exists():
221
+ click.echo("No cron jobs persisted.")
222
+ return
223
+ data = _json.loads(cron_file.read_text(encoding="utf-8"))
224
+ kept = [j for j in data if j["name"] != name]
225
+ if len(kept) == len(data):
226
+ click.echo(f"No job named '{name}'.")
227
+ return
228
+ cron_file.write_text(_json.dumps(kept, indent=2), encoding="utf-8")
229
+ click.echo(f"Removed cron job '{name}'.")
230
+
231
+
232
+ @cron.command("status")
233
+ @click.option("--path", default=None, help="Workspace directory.")
234
+ def cron_status(path: str | None) -> None:
235
+ """Show loaded cron jobs (live in-process schedules)."""
236
+ from .modules.system.cron import CronScheduler
237
+
238
+ cfg = _load_cfg(path)
239
+ cron_file = cfg.workspace / ".opencommand" / "cron.json"
240
+ sched = CronScheduler.load(cron_file)
241
+ click.echo(sched.status())
242
+
243
+
244
+ @cron.command("start")
245
+ @click.option("--path", default=None, help="Workspace directory.")
246
+ def cron_start(path: str | None) -> None:
247
+ """Load persisted cron jobs and run the scheduler in the foreground."""
248
+ import asyncio
249
+
250
+ from .modules.system.cron import CronScheduler
251
+
252
+ cfg = _load_cfg(path)
253
+ cron_file = cfg.workspace / ".opencommand" / "cron.json"
254
+ sched = CronScheduler.load(cron_file)
255
+ if not sched._jobs:
256
+ click.echo("No persisted cron jobs. Use `cron add` first.")
257
+ return
258
+ click.echo(sched.status())
259
+ click.echo("Running scheduler (Ctrl-C to stop)...")
260
+
261
+ async def _run() -> None:
262
+ sched.start()
263
+ try:
264
+ while not sched._stop.is_set():
265
+ await asyncio.sleep(sched._tick)
266
+ finally:
267
+ sched.stop()
268
+
269
+ try:
270
+ asyncio.run(_run())
271
+ except (KeyboardInterrupt, asyncio.CancelledError):
272
+ pass
273
+ click.echo("Scheduler stopped.")
274
+
275
+
276
+ @cli.command()
277
+ @click.argument("goal")
278
+ @click.option("--path", default=None, help="Workspace directory.")
279
+ @click.option("--max-iter", default=5, help="Automatic-mode iterations.")
280
+ @click.option("--verify", default=None, help="Shell command to verify success (e.g. 'pytest -q').")
281
+ @click.option("--pool", default=None, type=int, help="Worker model pool size (parallelism).")
282
+ @click.option("--no-vision", is_flag=True, help="Disable the desktop/vision tool.")
283
+ @click.option("--vision-model", default=None,
284
+ type=click.Choice(["vision", "vision_small"]),
285
+ help="Vision model for the desktop tool (vision=Qwen3-VL-4B, "
286
+ "vision_small=MiniCPM-V-4.6 0.8B for CPU/low-RAM).")
287
+ @click.option("--pipeline", default=None,
288
+ type=click.Choice(["standard", "advanced"]),
289
+ help="Pipeline: standard (plan->research->workers) or advanced "
290
+ "(Phase 6: scaffold + research-context + commander review gate).")
291
+ @click.option("--live-verify", is_flag=True,
292
+ help="After tests pass, run the desktop tool to interactively verify UI goals.")
293
+ def run(goal: str, path: str | None, max_iter: int, verify: str | None,
294
+ pool: int | None, no_vision: bool, vision_model: str | None,
295
+ pipeline: str | None, live_verify: bool) -> None:
296
+ """Run a GOAL autonomously using embedded models."""
297
+ import asyncio
298
+
299
+ from .modules.system.automatic import run_automatic
300
+ from .swarm.agents.commander import Commander
301
+
302
+ cfg = _load_cfg(path)
303
+ if no_vision:
304
+ cfg.vision = None
305
+ if vision_model:
306
+ cfg.vision_model = vision_model
307
+ if pipeline:
308
+ cfg.pipeline = pipeline
309
+ if live_verify:
310
+ cfg.live_verify = True
311
+ engine = Engine(model_dir=cfg.workspace / "models",
312
+ pool_size=pool or cfg.max_workers)
313
+ sup = Supervisor(max_retries=cfg.max_retries)
314
+ tools = _build_tools(engine, cfg)
315
+ commander = Commander(engine, sup, tools, cfg.workspace)
316
+ # Boot persisted cron jobs: they run inside the agent event loop and dispatch
317
+ # goals through the Commander (or run shell commands) when they fire.
318
+ from .modules.system.cron import CronScheduler
319
+ cron_file = cfg.workspace / ".opencommand" / "cron.json"
320
+ cron = CronScheduler.load(cron_file)
321
+ if cron._jobs:
322
+ cron.on_goal = lambda g: run_automatic(
323
+ commander, g, max_iterations=max_iter, verify_cmd=verify,
324
+ pipeline=cfg.pipeline, live_verify=cfg.live_verify)
325
+ click.echo(click.style(f"Loaded {len(cron._jobs)} cron job(s).", fg="green"))
326
+
327
+ async def _main() -> str:
328
+ if cron._jobs:
329
+ cron.start()
330
+ try:
331
+ return await run_automatic(
332
+ commander, goal, max_iterations=max_iter, verify_cmd=verify,
333
+ pipeline=cfg.pipeline, live_verify=cfg.live_verify)
334
+ finally:
335
+ cron.stop()
336
+ sup.shutdown()
337
+
338
+ click.echo(f"OpenCommand on {describe()}")
339
+ click.echo(click.style(f"Pipeline: {cfg.pipeline}"
340
+ + (" + live-verify" if cfg.live_verify else ""), fg="cyan"))
341
+ click.echo(click.style("Loading embedded models...", fg="cyan"))
342
+ click.echo(click.style("Running automatic mode...", fg="cyan"))
343
+ report = asyncio.run(_main())
344
+ click.echo(report)
345
+
346
+
347
+ @cli.command()
348
+ def tui() -> None:
349
+ """Launch the live swarm dashboard (asciimatics)."""
350
+ from .tui.dashboard import run_dashboard
351
+
352
+ run_dashboard()
353
+
354
+
355
+ @cli.command()
356
+ @click.argument("message")
357
+ @click.option("--title", default=None, help="Notification title.")
358
+ @click.option("--priority", default=3, help="ntfy priority 1..5 (default 3).")
359
+ def notify(message: str, title: str | None, priority: int) -> None:
360
+ """Send a test ntfy notification (needs NTFY_TOPIC env var)."""
361
+ from .modules.system.notify import Notifier
362
+
363
+ n = Notifier()
364
+ if not n.enabled:
365
+ click.echo(
366
+ "ntfy not configured. Set NTFY_TOPIC (e.g. "
367
+ "https://ntfy.sh/your-topic) in the environment."
368
+ )
369
+ return
370
+ ok = n.send(message, title=title, priority=priority)
371
+ click.echo("Notification sent." if ok else "Notification FAILED.")
372
+
373
+
374
+ if __name__ == "__main__": # pragma: no cover
375
+ cli()
@@ -0,0 +1,91 @@
1
+ """AgentLoop: the per-worker reasoning loop (prompt -> LLM -> tools -> observe)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+
7
+ from ..providers.provider import BaseProvider, ChatMessage
8
+ from .errors import ContextWindowError, RetryableError
9
+ from .tools import ToolRegistry, parse_tool_calls
10
+
11
+ SYSTEM_PROMPT = (
12
+ "You are an OpenCommand agent operating inside a workspace on {platform}. "
13
+ "You complete tasks by calling tools. Think step by step, then emit tool "
14
+ "calls. When the task is fully done, reply with a final summary and no "
15
+ "tool calls. Be concise and correct; prefer running tests to claiming success."
16
+ )
17
+
18
+
19
+ @dataclass
20
+ class AgentState:
21
+ messages: list[ChatMessage] = field(default_factory=list)
22
+ steps: int = 0
23
+ max_steps: int = 40
24
+
25
+
26
+ class AgentLoop:
27
+ """Drives one worker: repeatedly calls the LLM and executes its tools."""
28
+
29
+ def __init__(
30
+ self,
31
+ provider: BaseProvider,
32
+ tools: ToolRegistry,
33
+ system: str | None = None,
34
+ platform: str = "this machine",
35
+ ) -> None:
36
+ self.provider = provider
37
+ self.tools = tools
38
+ self.system = system or SYSTEM_PROMPT.format(platform=platform)
39
+
40
+ async def run(self, task: str, max_steps: int = 40) -> str:
41
+ state = AgentState(max_steps=max_steps)
42
+ state.messages.append(ChatMessage(role="system", content=self.system))
43
+ state.messages.append(ChatMessage(role="user", content=task))
44
+ while state.steps < state.max_steps:
45
+ state.steps += 1
46
+ try:
47
+ msg = await self.provider.acomplete(state.messages, tools=self.tools.schemas())
48
+ except ContextWindowError:
49
+ # Bound history and retry instead of failing the whole run.
50
+ state.messages = _truncate(state.messages, keep=20)
51
+ state.messages.append(ChatMessage(
52
+ role="system",
53
+ content="Context was truncated to fit the model window. Continue."))
54
+ continue
55
+ except Exception as e: # noqa: BLE001
56
+ if "context" in str(e).lower():
57
+ raise ContextWindowError(str(e)) from e
58
+ raise RetryableError(str(e)) from e # noqa: TRY003
59
+ state.messages.append(msg)
60
+ calls = parse_tool_calls(msg)
61
+ if not calls:
62
+ # No structured calls. If the model emitted a tool-call marker in
63
+ # text but parsing failed, nudge it to retry rather than stop.
64
+ if "<function=" in (msg.content or "") or "<tool_call" in (msg.content or ""):
65
+ state.messages.append(ChatMessage(
66
+ role="system",
67
+ content=("Your tool call could not be parsed. Re-emit it using the "
68
+ "exact <tool_call><function=name><parameter=key>value"
69
+ "</parameter></function></tool_call> format, or reply with a "
70
+ "final summary if the task is done.")))
71
+ continue
72
+ return msg.content or "(no output)"
73
+ for call in calls:
74
+ result = await self.tools.adispatch(call)
75
+ obs = result.output if result.ok else f"ERROR: {result.error}"
76
+ state.messages.append(
77
+ ChatMessage(role="tool", content=obs, name=call.name, tool_call_id=call.id)
78
+ )
79
+ # Proactively cap history to stay within n_ctx on long tool loops.
80
+ if len(state.messages) > 24:
81
+ state.messages = _truncate(state.messages, keep=20)
82
+ return "(max steps reached)"
83
+
84
+
85
+ def _truncate(messages: list[ChatMessage], keep: int = 20) -> list[ChatMessage]:
86
+ """Keep system + recent messages to bound context on retry."""
87
+ if len(messages) <= keep:
88
+ return messages
89
+ head = [m for m in messages if m.role == "system"]
90
+ tail = messages[-(keep - len(head)) :]
91
+ return head + tail