loopengt 0.1.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.
- loopengt/__init__.py +31 -0
- loopengt/adapters/__init__.py +1 -0
- loopengt/adapters/antigravity/__init__.py +1 -0
- loopengt/adapters/antigravity/adapter.py +55 -0
- loopengt/adapters/antigravity/commands.py +21 -0
- loopengt/adapters/base.py +51 -0
- loopengt/adapters/claude_code/__init__.py +1 -0
- loopengt/adapters/claude_code/adapter.py +55 -0
- loopengt/adapters/claude_code/commands.py +16 -0
- loopengt/adapters/codex/__init__.py +1 -0
- loopengt/adapters/codex/adapter.py +52 -0
- loopengt/adapters/codex/commands.py +16 -0
- loopengt/adapters/cursor/__init__.py +1 -0
- loopengt/adapters/cursor/adapter.py +56 -0
- loopengt/adapters/cursor/commands.py +29 -0
- loopengt/adapters/generic/__init__.py +1 -0
- loopengt/adapters/generic/terminal.py +82 -0
- loopengt/cli/__init__.py +1 -0
- loopengt/cli/commands/__init__.py +1 -0
- loopengt/cli/commands/design.py +171 -0
- loopengt/cli/commands/doctor.py +110 -0
- loopengt/cli/commands/eval.py +105 -0
- loopengt/cli/commands/init.py +131 -0
- loopengt/cli/commands/mcp_serve.py +57 -0
- loopengt/cli/commands/run.py +99 -0
- loopengt/cli/commands/template.py +145 -0
- loopengt/cli/commands/trace.py +114 -0
- loopengt/cli/formatters.py +125 -0
- loopengt/cli/main.py +66 -0
- loopengt/core/__init__.py +1 -0
- loopengt/core/evals/__init__.py +1 -0
- loopengt/core/evals/judges.py +216 -0
- loopengt/core/evals/metrics.py +119 -0
- loopengt/core/evals/regression.py +157 -0
- loopengt/core/memory/__init__.py +1 -0
- loopengt/core/memory/retrieval.py +124 -0
- loopengt/core/memory/store.py +184 -0
- loopengt/core/memory/summarizer.py +97 -0
- loopengt/core/models/__init__.py +43 -0
- loopengt/core/models/agent.py +126 -0
- loopengt/core/models/loop_spec.py +251 -0
- loopengt/core/models/policy.py +131 -0
- loopengt/core/models/state.py +271 -0
- loopengt/core/models/tool.py +105 -0
- loopengt/core/runtime/__init__.py +1 -0
- loopengt/core/runtime/checkpoint.py +152 -0
- loopengt/core/runtime/executor.py +463 -0
- loopengt/core/runtime/handoff.py +139 -0
- loopengt/core/runtime/scheduler.py +168 -0
- loopengt/core/tracing/__init__.py +1 -0
- loopengt/core/tracing/events.py +95 -0
- loopengt/core/tracing/exporters.py +158 -0
- loopengt/core/tracing/store.py +202 -0
- loopengt/mcp/__init__.py +1 -0
- loopengt/mcp/client/__init__.py +1 -0
- loopengt/mcp/client/manager.py +118 -0
- loopengt/mcp/client/tools.py +107 -0
- loopengt/mcp/server/__init__.py +1 -0
- loopengt/mcp/server/prompts.py +82 -0
- loopengt/mcp/server/resources.py +75 -0
- loopengt/mcp/server/server.py +50 -0
- loopengt/mcp/server/tools.py +214 -0
- loopengt/mcp/shared/__init__.py +1 -0
- loopengt/mcp/shared/schemas.py +91 -0
- loopengt/plugins/__init__.py +1 -0
- loopengt/plugins/base.py +90 -0
- loopengt/plugins/loader.py +130 -0
- loopengt/plugins/manifest.py +70 -0
- loopengt/plugins/registry.py +146 -0
- loopengt/prompts/LOOPENGT.md +60 -0
- loopengt/prompts/__init__.py +1 -0
- loopengt/storage/__init__.py +1 -0
- loopengt/storage/jsonl.py +84 -0
- loopengt/storage/sqlite.py +102 -0
- loopengt/templates/__init__.py +1 -0
- loopengt/templates/builtins/handoff_loop/LOOPENGS.md +10 -0
- loopengt/templates/builtins/planner_executor/LOOPENGS.md +29 -0
- loopengt/templates/builtins/research_architect/LOOPENGS.md +17 -0
- loopengt/templates/builtins/reviewer_retry/LOOPENGS.md +29 -0
- loopengt/templates/builtins/supervisor_workers/LOOPENGS.md +29 -0
- loopengt/templates/loader.py +38 -0
- loopengt/templates/registry.py +85 -0
- loopengt-0.1.0.dist-info/METADATA +275 -0
- loopengt-0.1.0.dist-info/RECORD +87 -0
- loopengt-0.1.0.dist-info/WHEEL +4 -0
- loopengt-0.1.0.dist-info/entry_points.txt +8 -0
- loopengt-0.1.0.dist-info/licenses/LICENSE +674 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""``loopengt doctor`` — diagnose configuration and dependencies."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
from loopengt.cli.formatters import print_doctor_result, print_header
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def doctor_cmd() -> None:
|
|
14
|
+
"""Diagnose loopengt configuration, dependencies, and adapters."""
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
print_header("loopengt doctor", "Checking configuration and dependencies")
|
|
18
|
+
|
|
19
|
+
# 1. Python version
|
|
20
|
+
py_version = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
|
|
21
|
+
py_ok = sys.version_info >= (3, 11)
|
|
22
|
+
print_doctor_result(
|
|
23
|
+
"Python version",
|
|
24
|
+
py_ok,
|
|
25
|
+
f"{py_version} {'(>= 3.11 required)' if not py_ok else ''}",
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# 2. Core dependencies
|
|
29
|
+
core_deps = ["pydantic", "typer", "rich", "structlog", "anyio", "yaml"]
|
|
30
|
+
for dep in core_deps:
|
|
31
|
+
try:
|
|
32
|
+
mod = importlib.import_module(dep)
|
|
33
|
+
version = getattr(mod, "__version__", "installed")
|
|
34
|
+
print_doctor_result(f" {dep}", True, version)
|
|
35
|
+
except ImportError:
|
|
36
|
+
print_doctor_result(f" {dep}", False, "not installed")
|
|
37
|
+
|
|
38
|
+
# 3. Optional dependencies
|
|
39
|
+
optional_deps = {
|
|
40
|
+
"mcp": ["fastmcp", "mcp"],
|
|
41
|
+
"llm": ["openai", "httpx"],
|
|
42
|
+
}
|
|
43
|
+
for group, deps in optional_deps.items():
|
|
44
|
+
for dep in deps:
|
|
45
|
+
try:
|
|
46
|
+
mod = importlib.import_module(dep)
|
|
47
|
+
version = getattr(mod, "__version__", "installed")
|
|
48
|
+
print_doctor_result(f" {dep} [{group}]", True, version)
|
|
49
|
+
except ImportError:
|
|
50
|
+
print_doctor_result(
|
|
51
|
+
f" {dep} [{group}]", False, f"install with: uv pip install \"loopengt[{group}]\""
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# 4. .loopengt/ directory
|
|
55
|
+
loopengt_dir = Path(".loopengt")
|
|
56
|
+
print_doctor_result(
|
|
57
|
+
".loopengt/ directory",
|
|
58
|
+
loopengt_dir.exists(),
|
|
59
|
+
str(loopengt_dir.resolve()) if loopengt_dir.exists() else "run 'loopengt init'",
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
if loopengt_dir.exists():
|
|
63
|
+
config_path = loopengt_dir / "config.toml"
|
|
64
|
+
print_doctor_result(
|
|
65
|
+
" config.toml",
|
|
66
|
+
config_path.exists(),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
runs_dir = loopengt_dir / "runs"
|
|
70
|
+
run_count = len(list(runs_dir.glob("*.json"))) if runs_dir.exists() else 0
|
|
71
|
+
print_doctor_result(
|
|
72
|
+
" runs/",
|
|
73
|
+
runs_dir.exists(),
|
|
74
|
+
f"{run_count} trace(s)" if runs_dir.exists() else "",
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# 5. Plugin entry points
|
|
78
|
+
try:
|
|
79
|
+
if sys.version_info >= (3, 12):
|
|
80
|
+
from importlib.metadata import entry_points
|
|
81
|
+
adapters = entry_points(group="loopengt.adapters")
|
|
82
|
+
else:
|
|
83
|
+
from importlib.metadata import entry_points
|
|
84
|
+
eps = entry_points()
|
|
85
|
+
adapters = eps.get("loopengt.adapters", [])
|
|
86
|
+
|
|
87
|
+
adapter_names = [ep.name for ep in adapters]
|
|
88
|
+
print_doctor_result(
|
|
89
|
+
"Registered adapters",
|
|
90
|
+
len(adapter_names) > 0,
|
|
91
|
+
", ".join(adapter_names) if adapter_names else "none found",
|
|
92
|
+
)
|
|
93
|
+
except Exception: # noqa: BLE001
|
|
94
|
+
print_doctor_result("Registered adapters", False, "discovery failed")
|
|
95
|
+
|
|
96
|
+
# 6. Environment variables
|
|
97
|
+
import os
|
|
98
|
+
|
|
99
|
+
env_vars = {
|
|
100
|
+
"OPENAI_API_KEY": "OpenAI API key",
|
|
101
|
+
"HF_TOKEN": "Hugging Face API token",
|
|
102
|
+
"LOOPENGT_LLM_PROVIDER": "LLM provider override (e.g. openai, huggingface)",
|
|
103
|
+
}
|
|
104
|
+
for var, desc in env_vars.items():
|
|
105
|
+
present = os.environ.get(var) is not None
|
|
106
|
+
print_doctor_result(
|
|
107
|
+
f" ${var}",
|
|
108
|
+
present,
|
|
109
|
+
desc if not present else "set",
|
|
110
|
+
)
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""``loopengt eval`` — run evaluations against a completed run."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
from loopengt.cli.formatters import console, print_error, print_info, print_success
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def eval_cmd(
|
|
14
|
+
run_id: str = typer.Argument(..., help="Run ID to evaluate."),
|
|
15
|
+
runs_dir: Path = typer.Option(
|
|
16
|
+
Path(".loopengt/runs"),
|
|
17
|
+
"--runs-dir",
|
|
18
|
+
help="Directory containing run traces.",
|
|
19
|
+
),
|
|
20
|
+
metrics: list[str] = typer.Option(
|
|
21
|
+
[],
|
|
22
|
+
"--metric",
|
|
23
|
+
"-m",
|
|
24
|
+
help="Specific metrics to evaluate (default: all).",
|
|
25
|
+
),
|
|
26
|
+
) -> None:
|
|
27
|
+
"""Run evaluations against a completed loop run."""
|
|
28
|
+
trace_path = runs_dir / f"{run_id}.json"
|
|
29
|
+
|
|
30
|
+
if not trace_path.exists():
|
|
31
|
+
candidates = list(runs_dir.glob(f"{run_id}*"))
|
|
32
|
+
if candidates:
|
|
33
|
+
trace_path = candidates[0]
|
|
34
|
+
else:
|
|
35
|
+
print_error(f"Trace not found: {trace_path}")
|
|
36
|
+
raise typer.Exit(code=1)
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
trace_data = json.loads(trace_path.read_text(encoding="utf-8"))
|
|
40
|
+
except (json.JSONDecodeError, OSError) as exc:
|
|
41
|
+
print_error(f"Failed to read trace: {exc}")
|
|
42
|
+
raise typer.Exit(code=1) from exc
|
|
43
|
+
|
|
44
|
+
print_info(f"Evaluating run {run_id}…")
|
|
45
|
+
|
|
46
|
+
# Run built-in metrics
|
|
47
|
+
results = _evaluate_builtin_metrics(trace_data, metrics or None)
|
|
48
|
+
|
|
49
|
+
# Display results
|
|
50
|
+
from rich.table import Table
|
|
51
|
+
|
|
52
|
+
table = Table(title="Evaluation Results", border_style="green")
|
|
53
|
+
table.add_column("Metric", style="cyan")
|
|
54
|
+
table.add_column("Value", style="white")
|
|
55
|
+
table.add_column("Status", style="green")
|
|
56
|
+
|
|
57
|
+
for name, value, status in results:
|
|
58
|
+
status_style = "green" if status == "pass" else "red"
|
|
59
|
+
table.add_row(name, str(value), f"[{status_style}]{status}[/{status_style}]")
|
|
60
|
+
|
|
61
|
+
console.print(table)
|
|
62
|
+
print_success(f"Evaluation complete for run {run_id}")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _evaluate_builtin_metrics(
|
|
66
|
+
trace_data: dict, metric_names: list[str] | None = None
|
|
67
|
+
) -> list[tuple[str, object, str]]:
|
|
68
|
+
"""Evaluate built-in metrics against a trace."""
|
|
69
|
+
results: list[tuple[str, object, str]] = []
|
|
70
|
+
|
|
71
|
+
all_metrics = {
|
|
72
|
+
"total_turns": lambda t: (t.get("turn", 0), "pass"),
|
|
73
|
+
"total_steps": lambda t: (len(t.get("step_states", {})), "pass"),
|
|
74
|
+
"completed_steps": lambda t: (
|
|
75
|
+
sum(
|
|
76
|
+
1
|
|
77
|
+
for s in t.get("step_states", {}).values()
|
|
78
|
+
if s.get("status") == "completed"
|
|
79
|
+
),
|
|
80
|
+
"pass",
|
|
81
|
+
),
|
|
82
|
+
"failed_steps": lambda t: (
|
|
83
|
+
(
|
|
84
|
+
n := sum(
|
|
85
|
+
1
|
|
86
|
+
for s in t.get("step_states", {}).values()
|
|
87
|
+
if s.get("status") == "failed"
|
|
88
|
+
)
|
|
89
|
+
),
|
|
90
|
+
"fail" if n > 0 else "pass",
|
|
91
|
+
),
|
|
92
|
+
"run_status": lambda t: (
|
|
93
|
+
t.get("status", "unknown"),
|
|
94
|
+
"pass" if t.get("status") == "completed" else "fail",
|
|
95
|
+
),
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
selected = metric_names or list(all_metrics.keys())
|
|
99
|
+
|
|
100
|
+
for name in selected:
|
|
101
|
+
if name in all_metrics:
|
|
102
|
+
value, status = all_metrics[name](trace_data)
|
|
103
|
+
results.append((name, value, status))
|
|
104
|
+
|
|
105
|
+
return results
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""``loopengt init`` — scaffold a new .loopengt/ project directory."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import shutil
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
from loopengt.cli.formatters import print_error, print_info, print_success
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def init_cmd(
|
|
14
|
+
directory: Path = typer.Argument(
|
|
15
|
+
Path("."),
|
|
16
|
+
help="Directory to initialise (defaults to current directory).",
|
|
17
|
+
),
|
|
18
|
+
force: bool = typer.Option(
|
|
19
|
+
False,
|
|
20
|
+
"--force",
|
|
21
|
+
"-f",
|
|
22
|
+
help="Overwrite existing .loopengt/ directory.",
|
|
23
|
+
),
|
|
24
|
+
) -> None:
|
|
25
|
+
"""Scaffold a new .loopengt/ project directory with default config."""
|
|
26
|
+
target = directory.resolve() / ".loopengt"
|
|
27
|
+
|
|
28
|
+
if target.exists() and not force:
|
|
29
|
+
print_error(
|
|
30
|
+
f"{target} already exists. Use --force to overwrite."
|
|
31
|
+
)
|
|
32
|
+
raise typer.Exit(code=1)
|
|
33
|
+
|
|
34
|
+
if target.exists() and force:
|
|
35
|
+
shutil.rmtree(target)
|
|
36
|
+
|
|
37
|
+
# Create directory structure
|
|
38
|
+
dirs = [
|
|
39
|
+
target,
|
|
40
|
+
target / "templates",
|
|
41
|
+
target / "runs",
|
|
42
|
+
target / "prompts",
|
|
43
|
+
]
|
|
44
|
+
for d in dirs:
|
|
45
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
46
|
+
|
|
47
|
+
# Write default config
|
|
48
|
+
config_path = target / "config.toml"
|
|
49
|
+
config_path.write_text(
|
|
50
|
+
'# Loop Engineering configuration\n'
|
|
51
|
+
'\n'
|
|
52
|
+
'[general]\n'
|
|
53
|
+
'default_pattern = "sequential"\n'
|
|
54
|
+
'trace_format = "jsonl"\n'
|
|
55
|
+
'\n'
|
|
56
|
+
'[llm]\n'
|
|
57
|
+
'provider = "openai"\n'
|
|
58
|
+
'model = "gpt-4o"\n'
|
|
59
|
+
'temperature = 0.7\n'
|
|
60
|
+
'# api_key_env = "OPENAI_API_KEY"\n'
|
|
61
|
+
'\n'
|
|
62
|
+
'[mcp]\n'
|
|
63
|
+
'transport = "stdio"\n'
|
|
64
|
+
'host = "localhost"\n'
|
|
65
|
+
'port = 8765\n'
|
|
66
|
+
'\n'
|
|
67
|
+
'[tracing]\n'
|
|
68
|
+
'backend = "jsonl"\n'
|
|
69
|
+
'directory = "runs"\n',
|
|
70
|
+
encoding="utf-8",
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# Write LOOPENGT prompt
|
|
74
|
+
loopengt_path = target / "prompts" / "LOOPENGT.md"
|
|
75
|
+
loopengt_path.write_text(
|
|
76
|
+
"# LOOPENGT — Loop Engineering Architect\n"
|
|
77
|
+
"\n"
|
|
78
|
+
"You are the Loop Engineering Architect. Given a goal, you design\n"
|
|
79
|
+
"the optimal agent loop by selecting the right pattern, defining\n"
|
|
80
|
+
"agents, steps, tools, and policies.\n"
|
|
81
|
+
"\n"
|
|
82
|
+
"## Principles\n"
|
|
83
|
+
"1. Minimal agents — each must justify its existence\n"
|
|
84
|
+
"2. Clear handoff contracts between steps\n"
|
|
85
|
+
"3. Verification gates at critical boundaries\n"
|
|
86
|
+
"4. Graceful degradation on failure\n"
|
|
87
|
+
"5. Observable by default (structured traces)\n",
|
|
88
|
+
encoding="utf-8",
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# Copy built-in templates
|
|
92
|
+
_copy_builtin_templates(target / "templates")
|
|
93
|
+
|
|
94
|
+
print_success(f"Initialised .loopengt/ at {target}")
|
|
95
|
+
print_info(" config.toml — edit LLM & tracing settings")
|
|
96
|
+
print_info(" prompts/ — architect & design prompts")
|
|
97
|
+
print_info(" templates/ — loop templates")
|
|
98
|
+
print_info(" runs/ — execution traces")
|
|
99
|
+
print_info("")
|
|
100
|
+
print_info("Next: loopengt design \"your goal here\"")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _copy_builtin_templates(templates_dir: Path) -> None:
|
|
104
|
+
"""Copy built-in templates into the project templates directory."""
|
|
105
|
+
try:
|
|
106
|
+
from importlib.resources import files
|
|
107
|
+
|
|
108
|
+
builtins_root = files("loopengt.templates.builtins")
|
|
109
|
+
# Iterate known template directories
|
|
110
|
+
for template_name in (
|
|
111
|
+
"planner_executor",
|
|
112
|
+
"reviewer_retry",
|
|
113
|
+
"supervisor_workers",
|
|
114
|
+
"research_architect",
|
|
115
|
+
"handoff_loop",
|
|
116
|
+
):
|
|
117
|
+
try:
|
|
118
|
+
template_pkg = builtins_root.joinpath(template_name) # type: ignore[union-attr]
|
|
119
|
+
dest = templates_dir / template_name
|
|
120
|
+
dest.mkdir(parents=True, exist_ok=True)
|
|
121
|
+
|
|
122
|
+
for filename in ("LOOPENGTS.md", "loop.yaml"):
|
|
123
|
+
resource = template_pkg.joinpath(filename) # type: ignore[union-attr]
|
|
124
|
+
resource_text = resource.read_text(encoding="utf-8") # type: ignore[union-attr]
|
|
125
|
+
(dest / filename).write_text(resource_text, encoding="utf-8")
|
|
126
|
+
except Exception: # noqa: BLE001
|
|
127
|
+
# Template not found — skip silently during init
|
|
128
|
+
continue
|
|
129
|
+
except Exception: # noqa: BLE001
|
|
130
|
+
# importlib.resources not available or templates not packaged yet
|
|
131
|
+
pass
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""``loopengt mcp`` — start the MCP server."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from loopengt.cli.formatters import print_error, print_info
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def mcp_cmd(
|
|
11
|
+
transport: str = typer.Option(
|
|
12
|
+
"stdio",
|
|
13
|
+
"--transport",
|
|
14
|
+
"-t",
|
|
15
|
+
help="Transport protocol: stdio or sse.",
|
|
16
|
+
),
|
|
17
|
+
host: str = typer.Option(
|
|
18
|
+
"localhost",
|
|
19
|
+
"--host",
|
|
20
|
+
help="Host for SSE transport.",
|
|
21
|
+
),
|
|
22
|
+
port: int = typer.Option(
|
|
23
|
+
8765,
|
|
24
|
+
"--port",
|
|
25
|
+
"-p",
|
|
26
|
+
help="Port for SSE transport.",
|
|
27
|
+
),
|
|
28
|
+
) -> None:
|
|
29
|
+
"""Start the loopengt MCP server."""
|
|
30
|
+
print_info(f"Starting MCP server (transport={transport})…")
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
from loopengt.mcp.server.server import create_server
|
|
34
|
+
|
|
35
|
+
server = create_server()
|
|
36
|
+
|
|
37
|
+
if transport == "stdio":
|
|
38
|
+
import asyncio
|
|
39
|
+
|
|
40
|
+
asyncio.run(server.run_stdio())
|
|
41
|
+
elif transport == "sse":
|
|
42
|
+
import asyncio
|
|
43
|
+
|
|
44
|
+
asyncio.run(server.run_sse(host=host, port=port))
|
|
45
|
+
else:
|
|
46
|
+
print_error(f"Unknown transport: {transport}")
|
|
47
|
+
raise typer.Exit(code=1)
|
|
48
|
+
|
|
49
|
+
except ImportError:
|
|
50
|
+
print_error(
|
|
51
|
+
"MCP dependencies not installed. "
|
|
52
|
+
"Install with: uv pip install \"loopengt[mcp]\""
|
|
53
|
+
)
|
|
54
|
+
raise typer.Exit(code=1)
|
|
55
|
+
except Exception as exc: # noqa: BLE001
|
|
56
|
+
print_error(f"MCP server failed: {exc}")
|
|
57
|
+
raise typer.Exit(code=1) from exc
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""``loopengt run`` — execute a loop from a YAML spec."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
import yaml
|
|
10
|
+
|
|
11
|
+
from loopengt.cli.formatters import print_error, print_info, print_success
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def run_cmd(
|
|
15
|
+
spec_path: Path = typer.Argument(
|
|
16
|
+
..., help="Path to loop.yaml spec file."
|
|
17
|
+
),
|
|
18
|
+
dry_run: bool = typer.Option(
|
|
19
|
+
False,
|
|
20
|
+
"--dry-run",
|
|
21
|
+
help="Validate the spec and show the execution plan without running.",
|
|
22
|
+
),
|
|
23
|
+
verbose: bool = typer.Option(
|
|
24
|
+
False, "--verbose", "-V", help="Enable verbose output."
|
|
25
|
+
),
|
|
26
|
+
) -> None:
|
|
27
|
+
"""Execute a loop from a YAML specification file."""
|
|
28
|
+
if not spec_path.exists():
|
|
29
|
+
print_error(f"Spec file not found: {spec_path}")
|
|
30
|
+
raise typer.Exit(code=1)
|
|
31
|
+
|
|
32
|
+
print_info(f"Loading spec from {spec_path}")
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
raw = yaml.safe_load(spec_path.read_text(encoding="utf-8"))
|
|
36
|
+
except yaml.YAMLError as exc:
|
|
37
|
+
print_error(f"Invalid YAML: {exc}")
|
|
38
|
+
raise typer.Exit(code=1) from exc
|
|
39
|
+
|
|
40
|
+
# Validate with Pydantic
|
|
41
|
+
try:
|
|
42
|
+
from loopengt.core.models.loop_spec import LoopSpec
|
|
43
|
+
|
|
44
|
+
spec = LoopSpec.model_validate(raw)
|
|
45
|
+
except Exception as exc: # noqa: BLE001
|
|
46
|
+
print_error(f"Spec validation failed: {exc}")
|
|
47
|
+
raise typer.Exit(code=1) from exc
|
|
48
|
+
|
|
49
|
+
print_success(f"Spec validated: {spec.name} v{spec.version}")
|
|
50
|
+
print_info(f" Pattern : {spec.pattern}")
|
|
51
|
+
print_info(f" Agents : {', '.join(spec.agent_names())}")
|
|
52
|
+
print_info(f" Steps : {', '.join(spec.step_names())}")
|
|
53
|
+
|
|
54
|
+
if dry_run:
|
|
55
|
+
print_info("Dry run — skipping execution.")
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
# Execute the loop
|
|
59
|
+
print_info("Starting loop execution…")
|
|
60
|
+
try:
|
|
61
|
+
from loopengt.core.runtime.executor import LoopExecutor
|
|
62
|
+
|
|
63
|
+
executor = LoopExecutor(spec)
|
|
64
|
+
result = asyncio.run(executor.execute())
|
|
65
|
+
|
|
66
|
+
if result.status.value == "completed":
|
|
67
|
+
print_success(f"Loop completed — run ID: {result.run_id}")
|
|
68
|
+
else:
|
|
69
|
+
print_error(f"Loop finished with status: {result.status}")
|
|
70
|
+
if result.error:
|
|
71
|
+
print_error(f" Error: {result.error}")
|
|
72
|
+
|
|
73
|
+
# Save trace
|
|
74
|
+
_save_trace(result, spec_path)
|
|
75
|
+
|
|
76
|
+
except Exception as exc: # noqa: BLE001
|
|
77
|
+
print_error(f"Execution failed: {exc}")
|
|
78
|
+
raise typer.Exit(code=1) from exc
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _save_trace(state: object, spec_path: Path) -> None:
|
|
82
|
+
"""Persist the run trace to .loopengt/runs/."""
|
|
83
|
+
import json
|
|
84
|
+
|
|
85
|
+
loopengt_dir = spec_path.parent / ".loopengt" / "runs"
|
|
86
|
+
loopengt_dir.mkdir(parents=True, exist_ok=True)
|
|
87
|
+
|
|
88
|
+
# state is a LoopState; serialise via Pydantic
|
|
89
|
+
try:
|
|
90
|
+
state_dict = state.model_dump(mode="json") # type: ignore[union-attr]
|
|
91
|
+
run_id = state_dict.get("run_id", "unknown")
|
|
92
|
+
trace_path = loopengt_dir / f"{run_id}.json"
|
|
93
|
+
trace_path.write_text(
|
|
94
|
+
json.dumps(state_dict, indent=2, default=str),
|
|
95
|
+
encoding="utf-8",
|
|
96
|
+
)
|
|
97
|
+
print_info(f"Trace saved to {trace_path}")
|
|
98
|
+
except Exception: # noqa: BLE001
|
|
99
|
+
pass
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""``loopengt template`` — manage loop templates."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from loopengt.cli.formatters import console, print_error, print_info, print_success
|
|
10
|
+
|
|
11
|
+
template_app = typer.Typer(
|
|
12
|
+
name="template",
|
|
13
|
+
help="Manage loop templates.",
|
|
14
|
+
no_args_is_help=True,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@template_app.command("list")
|
|
19
|
+
def template_list(
|
|
20
|
+
include_builtins: bool = typer.Option(
|
|
21
|
+
True, "--builtins/--no-builtins", help="Include built-in templates."
|
|
22
|
+
),
|
|
23
|
+
) -> None:
|
|
24
|
+
"""List available loop templates."""
|
|
25
|
+
from rich.table import Table
|
|
26
|
+
|
|
27
|
+
table = Table(title="Available Templates", border_style="blue")
|
|
28
|
+
table.add_column("Name", style="cyan")
|
|
29
|
+
table.add_column("Source", style="dim")
|
|
30
|
+
table.add_column("Description", style="white")
|
|
31
|
+
|
|
32
|
+
# Built-in templates
|
|
33
|
+
if include_builtins:
|
|
34
|
+
builtins = {
|
|
35
|
+
"planner_executor": "Sequential plan-then-execute pattern",
|
|
36
|
+
"reviewer_retry": "Code review with iterative retry on failure",
|
|
37
|
+
"supervisor_workers": "Supervisor delegates to specialised workers",
|
|
38
|
+
"research_architect": "Research → synthesise → architect workflow",
|
|
39
|
+
"handoff_loop": "Sequential agent-to-agent handoff pattern",
|
|
40
|
+
}
|
|
41
|
+
for name, desc in builtins.items():
|
|
42
|
+
table.add_row(name, "builtin", desc)
|
|
43
|
+
|
|
44
|
+
# Project-local templates
|
|
45
|
+
local_dir = Path(".loopengt/templates")
|
|
46
|
+
if local_dir.exists():
|
|
47
|
+
for entry in sorted(local_dir.iterdir()):
|
|
48
|
+
if entry.is_dir() and (entry / "loop.yaml").exists():
|
|
49
|
+
if entry.name not in (
|
|
50
|
+
"planner_executor",
|
|
51
|
+
"reviewer_retry",
|
|
52
|
+
"supervisor_workers",
|
|
53
|
+
"research_architect",
|
|
54
|
+
"handoff_loop",
|
|
55
|
+
):
|
|
56
|
+
table.add_row(entry.name, "local", "")
|
|
57
|
+
|
|
58
|
+
console.print(table)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@template_app.command("use")
|
|
62
|
+
def template_use(
|
|
63
|
+
name: str = typer.Argument(..., help="Template name to use."),
|
|
64
|
+
output: Path = typer.Option(
|
|
65
|
+
Path("loop.yaml"),
|
|
66
|
+
"--output",
|
|
67
|
+
"-o",
|
|
68
|
+
help="Output path for the loop.yaml.",
|
|
69
|
+
),
|
|
70
|
+
) -> None:
|
|
71
|
+
"""Create a loop.yaml from a named template."""
|
|
72
|
+
import shutil
|
|
73
|
+
|
|
74
|
+
# Search in project-local templates first, then built-ins
|
|
75
|
+
search_paths = [
|
|
76
|
+
Path(".loopengt/templates") / name,
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
# Add built-in template paths
|
|
80
|
+
try:
|
|
81
|
+
from importlib.resources import files
|
|
82
|
+
|
|
83
|
+
builtin_path = files("loopengt.templates.builtins").joinpath(name) # type: ignore[union-attr]
|
|
84
|
+
search_paths.append(Path(str(builtin_path)))
|
|
85
|
+
except Exception: # noqa: BLE001
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
for template_dir in search_paths:
|
|
89
|
+
src = template_dir / "loop.yaml" # type: ignore[operator]
|
|
90
|
+
if hasattr(src, "exists") and src.exists(): # type: ignore[union-attr]
|
|
91
|
+
try:
|
|
92
|
+
if isinstance(src, Path):
|
|
93
|
+
shutil.copy2(src, output)
|
|
94
|
+
else:
|
|
95
|
+
# importlib resource
|
|
96
|
+
output.write_text(src.read_text(encoding="utf-8"), encoding="utf-8") # type: ignore[union-attr]
|
|
97
|
+
print_success(f"Template '{name}' written to {output}")
|
|
98
|
+
return
|
|
99
|
+
except Exception as exc: # noqa: BLE001
|
|
100
|
+
print_error(f"Failed to copy template: {exc}")
|
|
101
|
+
raise typer.Exit(code=1) from exc
|
|
102
|
+
|
|
103
|
+
print_error(f"Template not found: {name}")
|
|
104
|
+
print_info("Use 'loopengt template list' to see available templates.")
|
|
105
|
+
raise typer.Exit(code=1)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@template_app.command("create")
|
|
109
|
+
def template_create(
|
|
110
|
+
name: str = typer.Argument(..., help="Name for the new template."),
|
|
111
|
+
source: Path = typer.Option(
|
|
112
|
+
Path("loop.yaml"),
|
|
113
|
+
"--source",
|
|
114
|
+
"-s",
|
|
115
|
+
help="Source loop.yaml to create template from.",
|
|
116
|
+
),
|
|
117
|
+
) -> None:
|
|
118
|
+
"""Create a new template from an existing loop.yaml."""
|
|
119
|
+
if not source.exists():
|
|
120
|
+
print_error(f"Source file not found: {source}")
|
|
121
|
+
raise typer.Exit(code=1)
|
|
122
|
+
|
|
123
|
+
template_dir = Path(".loopengt/templates") / name
|
|
124
|
+
template_dir.mkdir(parents=True, exist_ok=True)
|
|
125
|
+
|
|
126
|
+
import shutil
|
|
127
|
+
|
|
128
|
+
shutil.copy2(source, template_dir / "loop.yaml")
|
|
129
|
+
|
|
130
|
+
# Create a stub LOOPENGTS.md
|
|
131
|
+
(template_dir / "LOOPENGTS.md").write_text(
|
|
132
|
+
f"# {name}\n\n"
|
|
133
|
+
f"## Purpose\n"
|
|
134
|
+
f"TODO: Describe the purpose of this template.\n\n"
|
|
135
|
+
f"## Agents\n"
|
|
136
|
+
f"TODO: Document the agents and their roles.\n\n"
|
|
137
|
+
f"## Steps\n"
|
|
138
|
+
f"TODO: Document the execution steps.\n\n"
|
|
139
|
+
f"## Verification\n"
|
|
140
|
+
f"TODO: Describe verification criteria.\n",
|
|
141
|
+
encoding="utf-8",
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
print_success(f"Template '{name}' created at {template_dir}")
|
|
145
|
+
print_info(f"Edit {template_dir / 'LOOPENGTS.md'} to add documentation.")
|