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
loopengt/__init__.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Loop Engineering Agent — design, orchestrate, and evaluate agent loops."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__version__ = "0.1.0"
|
|
6
|
+
__all__ = [
|
|
7
|
+
"__version__",
|
|
8
|
+
"LoopSpec",
|
|
9
|
+
"LoopState",
|
|
10
|
+
"AgentRole",
|
|
11
|
+
"ToolSpec",
|
|
12
|
+
"Policy",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
# Lazy imports to keep startup fast
|
|
16
|
+
def __getattr__(name: str) -> object:
|
|
17
|
+
"""Lazy-load public API symbols."""
|
|
18
|
+
_imports: dict[str, tuple[str, str]] = {
|
|
19
|
+
"LoopSpec": ("loopengt.core.models.loop_spec", "LoopSpec"),
|
|
20
|
+
"LoopState": ("loopengt.core.models.state", "LoopState"),
|
|
21
|
+
"AgentRole": ("loopengt.core.models.agent", "AgentRole"),
|
|
22
|
+
"ToolSpec": ("loopengt.core.models.tool", "ToolSpec"),
|
|
23
|
+
"Policy": ("loopengt.core.models.policy", "Policy"),
|
|
24
|
+
}
|
|
25
|
+
if name in _imports:
|
|
26
|
+
module_path, attr_name = _imports[name]
|
|
27
|
+
import importlib
|
|
28
|
+
|
|
29
|
+
module = importlib.import_module(module_path)
|
|
30
|
+
return getattr(module, attr_name)
|
|
31
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""IDE adapters for loop engineering."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Antigravity IDE adapter."""
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Antigravity IDE adapter — skeleton implementation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from loopengt.plugins.base import Plugin
|
|
8
|
+
from loopengt.plugins.manifest import PluginManifest
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AntigravityAdapter(Plugin):
|
|
12
|
+
"""Adapter for Google Antigravity IDE.
|
|
13
|
+
|
|
14
|
+
Integrates loopengt with Antigravity's MCP tool system and
|
|
15
|
+
skill framework.
|
|
16
|
+
|
|
17
|
+
.. note:: Skeleton — requires Antigravity MCP integration.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def manifest(self) -> PluginManifest:
|
|
22
|
+
return PluginManifest(
|
|
23
|
+
name="antigravity",
|
|
24
|
+
version="0.1.0",
|
|
25
|
+
description="Antigravity IDE adapter for loopengt",
|
|
26
|
+
capabilities=["adapter", "commands", "mcp"],
|
|
27
|
+
entry_points=["loopengt.adapters"],
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
def activate(self, context: dict[str, Any]) -> None:
|
|
31
|
+
self._active = True
|
|
32
|
+
|
|
33
|
+
def deactivate(self) -> None:
|
|
34
|
+
self._active = False
|
|
35
|
+
|
|
36
|
+
def capabilities(self) -> set[str]:
|
|
37
|
+
return {"commands", "mcp", "file_access", "terminal"}
|
|
38
|
+
|
|
39
|
+
def register_commands(self, registry: Any) -> None:
|
|
40
|
+
raise NotImplementedError("Antigravity command registration not yet implemented")
|
|
41
|
+
|
|
42
|
+
def on_loop_start(self, loop_spec: dict[str, Any]) -> None:
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
def on_step_start(self, step: str, agent: str) -> None:
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
def on_step_complete(self, step: str, result: Any) -> None:
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
def on_loop_complete(self, result: Any) -> None:
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
def on_error(self, error: Exception, context: dict[str, Any]) -> None:
|
|
55
|
+
pass
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Antigravity command definitions — placeholder."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
ANTIGRAVITY_COMMANDS = [
|
|
6
|
+
{
|
|
7
|
+
"id": "loopengt.design",
|
|
8
|
+
"title": "Design Loop",
|
|
9
|
+
"description": "Design a new agent loop from a goal",
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"id": "loopengt.run",
|
|
13
|
+
"title": "Run Loop",
|
|
14
|
+
"description": "Execute a loop.yaml spec",
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"id": "loopengt.trace",
|
|
18
|
+
"title": "View Trace",
|
|
19
|
+
"description": "Inspect execution trace",
|
|
20
|
+
},
|
|
21
|
+
]
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Adapter protocol — the contract all IDE adapters must implement."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Protocol, runtime_checkable
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@runtime_checkable
|
|
9
|
+
class Adapter(Protocol):
|
|
10
|
+
"""Protocol for IDE/environment adapters.
|
|
11
|
+
|
|
12
|
+
Each adapter bridges loopengt's runtime with a specific IDE or
|
|
13
|
+
execution environment (terminal, Cursor, Claude Code, etc.).
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def capabilities(self) -> set[str]:
|
|
17
|
+
"""Return the set of capabilities this adapter provides.
|
|
18
|
+
|
|
19
|
+
Standard capabilities include:
|
|
20
|
+
- ``"commands"`` — can register user-facing commands
|
|
21
|
+
- ``"notifications"`` — can show notifications
|
|
22
|
+
- ``"progress"`` — can show progress indicators
|
|
23
|
+
- ``"file_access"`` — can read/write files
|
|
24
|
+
- ``"terminal"`` — has terminal access
|
|
25
|
+
- ``"mcp"`` — supports MCP protocol
|
|
26
|
+
"""
|
|
27
|
+
...
|
|
28
|
+
|
|
29
|
+
def register_commands(self, registry: Any) -> None:
|
|
30
|
+
"""Register adapter-specific commands with the given registry."""
|
|
31
|
+
...
|
|
32
|
+
|
|
33
|
+
def on_loop_start(self, loop_spec: dict[str, Any]) -> None:
|
|
34
|
+
"""Called when a loop begins execution."""
|
|
35
|
+
...
|
|
36
|
+
|
|
37
|
+
def on_step_start(self, step: str, agent: str) -> None:
|
|
38
|
+
"""Called when a step begins execution."""
|
|
39
|
+
...
|
|
40
|
+
|
|
41
|
+
def on_step_complete(self, step: str, result: Any) -> None:
|
|
42
|
+
"""Called when a step completes."""
|
|
43
|
+
...
|
|
44
|
+
|
|
45
|
+
def on_loop_complete(self, result: Any) -> None:
|
|
46
|
+
"""Called when the loop finishes."""
|
|
47
|
+
...
|
|
48
|
+
|
|
49
|
+
def on_error(self, error: Exception, context: dict[str, Any]) -> None:
|
|
50
|
+
"""Called when an error occurs during execution."""
|
|
51
|
+
...
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Claude Code adapter."""
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Claude Code adapter — skeleton implementation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from loopengt.plugins.base import Plugin
|
|
8
|
+
from loopengt.plugins.manifest import PluginManifest
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ClaudeCodeAdapter(Plugin):
|
|
12
|
+
"""Adapter for Claude Code (Anthropic's CLI-based coding agent).
|
|
13
|
+
|
|
14
|
+
Integrates loopengt with Claude Code's MCP tool capabilities
|
|
15
|
+
and permission model.
|
|
16
|
+
|
|
17
|
+
.. note:: Skeleton — requires Claude Code's MCP integration.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def manifest(self) -> PluginManifest:
|
|
22
|
+
return PluginManifest(
|
|
23
|
+
name="claude-code",
|
|
24
|
+
version="0.1.0",
|
|
25
|
+
description="Claude Code adapter for loopengt",
|
|
26
|
+
capabilities=["adapter", "commands", "mcp"],
|
|
27
|
+
entry_points=["loopengt.adapters"],
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
def activate(self, context: dict[str, Any]) -> None:
|
|
31
|
+
self._active = True
|
|
32
|
+
|
|
33
|
+
def deactivate(self) -> None:
|
|
34
|
+
self._active = False
|
|
35
|
+
|
|
36
|
+
def capabilities(self) -> set[str]:
|
|
37
|
+
return {"commands", "mcp", "file_access", "terminal"}
|
|
38
|
+
|
|
39
|
+
def register_commands(self, registry: Any) -> None:
|
|
40
|
+
raise NotImplementedError("Claude Code command registration not yet implemented")
|
|
41
|
+
|
|
42
|
+
def on_loop_start(self, loop_spec: dict[str, Any]) -> None:
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
def on_step_start(self, step: str, agent: str) -> None:
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
def on_step_complete(self, step: str, result: Any) -> None:
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
def on_loop_complete(self, result: Any) -> None:
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
def on_error(self, error: Exception, context: dict[str, Any]) -> None:
|
|
55
|
+
pass
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Claude Code command definitions — placeholder."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
CLAUDE_CODE_COMMANDS = [
|
|
6
|
+
{
|
|
7
|
+
"id": "loopengt.design",
|
|
8
|
+
"title": "Design Loop",
|
|
9
|
+
"description": "Design a new agent loop from a goal",
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"id": "loopengt.run",
|
|
13
|
+
"title": "Run Loop",
|
|
14
|
+
"description": "Execute a loop.yaml spec",
|
|
15
|
+
},
|
|
16
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Codex adapter."""
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Codex (OpenAI) adapter — skeleton implementation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from loopengt.plugins.base import Plugin
|
|
8
|
+
from loopengt.plugins.manifest import PluginManifest
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CodexAdapter(Plugin):
|
|
12
|
+
"""Adapter for OpenAI Codex agent environment.
|
|
13
|
+
|
|
14
|
+
.. note:: Skeleton — requires Codex CLI integration.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def manifest(self) -> PluginManifest:
|
|
19
|
+
return PluginManifest(
|
|
20
|
+
name="codex",
|
|
21
|
+
version="0.1.0",
|
|
22
|
+
description="Codex adapter for loopengt",
|
|
23
|
+
capabilities=["adapter", "commands"],
|
|
24
|
+
entry_points=["loopengt.adapters"],
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
def activate(self, context: dict[str, Any]) -> None:
|
|
28
|
+
self._active = True
|
|
29
|
+
|
|
30
|
+
def deactivate(self) -> None:
|
|
31
|
+
self._active = False
|
|
32
|
+
|
|
33
|
+
def capabilities(self) -> set[str]:
|
|
34
|
+
return {"commands", "file_access"}
|
|
35
|
+
|
|
36
|
+
def register_commands(self, registry: Any) -> None:
|
|
37
|
+
raise NotImplementedError("Codex command registration not yet implemented")
|
|
38
|
+
|
|
39
|
+
def on_loop_start(self, loop_spec: dict[str, Any]) -> None:
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
def on_step_start(self, step: str, agent: str) -> None:
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
def on_step_complete(self, step: str, result: Any) -> None:
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
def on_loop_complete(self, result: Any) -> None:
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
def on_error(self, error: Exception, context: dict[str, Any]) -> None:
|
|
52
|
+
pass
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Codex command definitions — placeholder."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
CODEX_COMMANDS = [
|
|
6
|
+
{
|
|
7
|
+
"id": "loopengt.design",
|
|
8
|
+
"title": "Design Loop",
|
|
9
|
+
"description": "Design a new agent loop",
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"id": "loopengt.run",
|
|
13
|
+
"title": "Run Loop",
|
|
14
|
+
"description": "Execute a loop spec",
|
|
15
|
+
},
|
|
16
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Cursor IDE adapter."""
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Cursor IDE adapter — skeleton implementation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from loopengt.plugins.base import Plugin
|
|
8
|
+
from loopengt.plugins.manifest import PluginManifest
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CursorAdapter(Plugin):
|
|
12
|
+
"""Adapter for the Cursor IDE.
|
|
13
|
+
|
|
14
|
+
Registers loopengt commands accessible via Cursor's command palette
|
|
15
|
+
and integrates with Cursor's agent capabilities.
|
|
16
|
+
|
|
17
|
+
.. note:: This is a skeleton implementation. Full integration
|
|
18
|
+
requires the Cursor extension API.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def manifest(self) -> PluginManifest:
|
|
23
|
+
return PluginManifest(
|
|
24
|
+
name="cursor",
|
|
25
|
+
version="0.1.0",
|
|
26
|
+
description="Cursor IDE adapter for loopengt",
|
|
27
|
+
capabilities=["adapter", "commands", "mcp"],
|
|
28
|
+
entry_points=["loopengt.adapters"],
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
def activate(self, context: dict[str, Any]) -> None:
|
|
32
|
+
self._active = True
|
|
33
|
+
|
|
34
|
+
def deactivate(self) -> None:
|
|
35
|
+
self._active = False
|
|
36
|
+
|
|
37
|
+
def capabilities(self) -> set[str]:
|
|
38
|
+
return {"commands", "mcp", "file_access"}
|
|
39
|
+
|
|
40
|
+
def register_commands(self, registry: Any) -> None:
|
|
41
|
+
raise NotImplementedError("Cursor command registration not yet implemented")
|
|
42
|
+
|
|
43
|
+
def on_loop_start(self, loop_spec: dict[str, Any]) -> None:
|
|
44
|
+
pass # Will integrate with Cursor notifications
|
|
45
|
+
|
|
46
|
+
def on_step_start(self, step: str, agent: str) -> None:
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
def on_step_complete(self, step: str, result: Any) -> None:
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
def on_loop_complete(self, result: Any) -> None:
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
def on_error(self, error: Exception, context: dict[str, Any]) -> None:
|
|
56
|
+
pass
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Cursor-specific command definitions — placeholder."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
# Cursor commands will be registered via the Cursor extension API.
|
|
6
|
+
# This module defines the command metadata for Cursor integration.
|
|
7
|
+
|
|
8
|
+
CURSOR_COMMANDS = [
|
|
9
|
+
{
|
|
10
|
+
"id": "loopengt.design",
|
|
11
|
+
"title": "LOOPENGT: Design Loop",
|
|
12
|
+
"description": "Design a new agent loop from a goal",
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"id": "loopengt.run",
|
|
16
|
+
"title": "LOOPENGT: Run Loop",
|
|
17
|
+
"description": "Execute a loop.yaml spec",
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"id": "loopengt.trace",
|
|
21
|
+
"title": "LOOPENGT: View Trace",
|
|
22
|
+
"description": "Inspect execution trace of a run",
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"id": "loopengt.template",
|
|
26
|
+
"title": "LOOPENGT: Use Template",
|
|
27
|
+
"description": "Create a loop from a template",
|
|
28
|
+
},
|
|
29
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Generic (non-IDE-specific) adapters."""
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Terminal/CLI adapter — the default adapter for command-line usage."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
|
|
9
|
+
from loopengt.adapters.base import Adapter
|
|
10
|
+
from loopengt.plugins.base import Plugin
|
|
11
|
+
from loopengt.plugins.manifest import PluginManifest
|
|
12
|
+
|
|
13
|
+
console = Console()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TerminalAdapter(Plugin):
|
|
17
|
+
"""Full terminal/CLI adapter for loopengt.
|
|
18
|
+
|
|
19
|
+
This is the default adapter used when running ``loopengt`` from the
|
|
20
|
+
command line. It outputs step progress to the terminal using Rich.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def manifest(self) -> PluginManifest:
|
|
25
|
+
return PluginManifest(
|
|
26
|
+
name="terminal",
|
|
27
|
+
version="0.1.0",
|
|
28
|
+
description="Terminal/CLI adapter for loopengt",
|
|
29
|
+
capabilities=["adapter", "commands", "notifications", "progress", "terminal"],
|
|
30
|
+
entry_points=["loopengt.adapters"],
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
def activate(self, context: dict[str, Any]) -> None:
|
|
34
|
+
"""Activate the terminal adapter."""
|
|
35
|
+
self._active = True
|
|
36
|
+
|
|
37
|
+
def deactivate(self) -> None:
|
|
38
|
+
"""Deactivate the terminal adapter."""
|
|
39
|
+
self._active = False
|
|
40
|
+
|
|
41
|
+
# ------------------------------------------------------------------
|
|
42
|
+
# Adapter protocol
|
|
43
|
+
# ------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
def capabilities(self) -> set[str]:
|
|
46
|
+
return {"commands", "notifications", "progress", "terminal"}
|
|
47
|
+
|
|
48
|
+
def register_commands(self, registry: Any) -> None:
|
|
49
|
+
"""Terminal adapter commands are registered via CLI; no-op here."""
|
|
50
|
+
|
|
51
|
+
def on_loop_start(self, loop_spec: dict[str, Any]) -> None:
|
|
52
|
+
name = loop_spec.get("name", "unknown")
|
|
53
|
+
console.print(f"\n[bold blue]▶ Loop starting:[/bold blue] {name}")
|
|
54
|
+
console.print(f" Pattern: {loop_spec.get('pattern', 'sequential')}")
|
|
55
|
+
agents = loop_spec.get("agents", [])
|
|
56
|
+
if agents:
|
|
57
|
+
names = [
|
|
58
|
+
a["name"] if isinstance(a, dict) else str(a) for a in agents
|
|
59
|
+
]
|
|
60
|
+
console.print(f" Agents: {', '.join(names)}")
|
|
61
|
+
console.print()
|
|
62
|
+
|
|
63
|
+
def on_step_start(self, step: str, agent: str) -> None:
|
|
64
|
+
console.print(f" [yellow]● Step:[/yellow] {step} [dim]agent={agent}[/dim]")
|
|
65
|
+
|
|
66
|
+
def on_step_complete(self, step: str, result: Any) -> None:
|
|
67
|
+
status = "✓" if result else "●"
|
|
68
|
+
color = "green" if result else "dim"
|
|
69
|
+
console.print(f" [{color}]{status} Completed:[/{color}] {step}")
|
|
70
|
+
|
|
71
|
+
def on_loop_complete(self, result: Any) -> None:
|
|
72
|
+
console.print(f"\n[bold green]✓ Loop completed[/bold green]")
|
|
73
|
+
if isinstance(result, dict):
|
|
74
|
+
run_id = result.get("run_id", "")
|
|
75
|
+
if run_id:
|
|
76
|
+
console.print(f" Run ID: {run_id}")
|
|
77
|
+
|
|
78
|
+
def on_error(self, error: Exception, context: dict[str, Any]) -> None:
|
|
79
|
+
console.print(f"\n[bold red]✗ Error:[/bold red] {error}")
|
|
80
|
+
step = context.get("step")
|
|
81
|
+
if step:
|
|
82
|
+
console.print(f" Step: {step}")
|
loopengt/cli/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CLI package for loopengt."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CLI command subpackage."""
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""``loopengt design`` — invoke LOOPENGT to generate a loop spec from a goal."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
import yaml
|
|
9
|
+
|
|
10
|
+
from loopengt.cli.formatters import print_error, print_info, print_success
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def design_cmd(
|
|
14
|
+
goal: str = typer.Argument(
|
|
15
|
+
..., help="Natural-language goal for the loop to achieve."
|
|
16
|
+
),
|
|
17
|
+
output: Path = typer.Option(
|
|
18
|
+
Path("loop.yaml"),
|
|
19
|
+
"--output",
|
|
20
|
+
"-o",
|
|
21
|
+
help="Output path for the generated loop.yaml.",
|
|
22
|
+
),
|
|
23
|
+
template: str | None = typer.Option(
|
|
24
|
+
None,
|
|
25
|
+
"--template",
|
|
26
|
+
"-t",
|
|
27
|
+
help="Start from a named template instead of blank.",
|
|
28
|
+
),
|
|
29
|
+
design_doc: Path = typer.Option(
|
|
30
|
+
Path("loopengt.md"),
|
|
31
|
+
"--design-doc",
|
|
32
|
+
"-d",
|
|
33
|
+
help="Output path for the design document.",
|
|
34
|
+
),
|
|
35
|
+
) -> None:
|
|
36
|
+
"""Design a loop spec from a natural-language goal.
|
|
37
|
+
|
|
38
|
+
Invokes the LOOPENGT architect prompt (or an LLM if configured) to
|
|
39
|
+
produce a ``loop.yaml`` and ``LOOP_DESIGN.md``.
|
|
40
|
+
"""
|
|
41
|
+
print_info(f"Designing loop for goal: {goal}")
|
|
42
|
+
|
|
43
|
+
# Try LLM-based design first
|
|
44
|
+
try:
|
|
45
|
+
from loopengt.core.designer import design_loop
|
|
46
|
+
|
|
47
|
+
spec_dict, design_markdown = design_loop(goal, template_name=template)
|
|
48
|
+
except ImportError:
|
|
49
|
+
# Fall back to template-based stub generation
|
|
50
|
+
spec_dict = _generate_stub_spec(goal, template)
|
|
51
|
+
design_markdown = _generate_stub_design(goal, spec_dict)
|
|
52
|
+
|
|
53
|
+
# Write loop.yaml
|
|
54
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
55
|
+
output.write_text(
|
|
56
|
+
yaml.dump(spec_dict, default_flow_style=False, sort_keys=False),
|
|
57
|
+
encoding="utf-8",
|
|
58
|
+
)
|
|
59
|
+
print_success(f"Loop spec written to {output}")
|
|
60
|
+
|
|
61
|
+
# Write design doc
|
|
62
|
+
design_doc.parent.mkdir(parents=True, exist_ok=True)
|
|
63
|
+
design_doc.write_text(design_markdown, encoding="utf-8")
|
|
64
|
+
print_success(f"Design document written to {design_doc}")
|
|
65
|
+
|
|
66
|
+
print_info("Next: loopengt run loop.yaml")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _generate_stub_spec(goal: str, template: str | None) -> dict:
|
|
70
|
+
"""Generate a minimal loop spec dict without an LLM."""
|
|
71
|
+
import re
|
|
72
|
+
|
|
73
|
+
# Derive a slug name from the goal
|
|
74
|
+
slug = re.sub(r"[^a-z0-9]+", "-", goal.lower()).strip("-")[:64]
|
|
75
|
+
|
|
76
|
+
spec: dict = {
|
|
77
|
+
"name": slug,
|
|
78
|
+
"version": "1.0",
|
|
79
|
+
"goal": goal,
|
|
80
|
+
"pattern": "sequential",
|
|
81
|
+
"agents": [
|
|
82
|
+
{
|
|
83
|
+
"name": "planner",
|
|
84
|
+
"description": f"Plans the approach for: {goal}",
|
|
85
|
+
"capabilities": ["planning"],
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"name": "executor",
|
|
89
|
+
"description": "Executes the plan produced by the planner",
|
|
90
|
+
"capabilities": ["code_generation", "tool_use"],
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
"steps": [
|
|
94
|
+
{
|
|
95
|
+
"name": "plan",
|
|
96
|
+
"description": "Analyse the goal and produce an action plan",
|
|
97
|
+
"agent": "planner",
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
"name": "execute",
|
|
101
|
+
"description": "Execute the plan",
|
|
102
|
+
"agent": "executor",
|
|
103
|
+
"dependencies": [{"step_name": "plan"}],
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
"policy": {
|
|
107
|
+
"max_turns": 20,
|
|
108
|
+
"max_total_time_seconds": 600,
|
|
109
|
+
},
|
|
110
|
+
"stop_conditions": [
|
|
111
|
+
{"condition_type": "max_turns", "value": 20},
|
|
112
|
+
{"condition_type": "goal_met", "value": "true"},
|
|
113
|
+
],
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if template:
|
|
117
|
+
spec["metadata"] = {"based_on_template": template}
|
|
118
|
+
|
|
119
|
+
return spec
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _generate_stub_design(goal: str, spec: dict) -> str:
|
|
123
|
+
"""Generate a stub loopengt.md without an LLM."""
|
|
124
|
+
agents_section = ""
|
|
125
|
+
for agent in spec.get("agents", []):
|
|
126
|
+
agents_section += (
|
|
127
|
+
f"### {agent['name']}\n"
|
|
128
|
+
f"- **Description**: {agent.get('description', 'N/A')}\n"
|
|
129
|
+
f"- **Capabilities**: {', '.join(agent.get('capabilities', []))}\n\n"
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
steps_section = ""
|
|
133
|
+
for i, step in enumerate(spec.get("steps", []), 1):
|
|
134
|
+
steps_section += (
|
|
135
|
+
f"{i}. **{step['name']}** — {step.get('description', 'N/A')}\n"
|
|
136
|
+
f" - Agent: `{step['agent']}`\n"
|
|
137
|
+
)
|
|
138
|
+
if step.get("dependencies"):
|
|
139
|
+
deps = ", ".join(d["step_name"] for d in step["dependencies"])
|
|
140
|
+
steps_section += f" - Depends on: {deps}\n"
|
|
141
|
+
steps_section += "\n"
|
|
142
|
+
|
|
143
|
+
return (
|
|
144
|
+
f"# Loop Design: {spec.get('name', 'untitled')}\n"
|
|
145
|
+
f"\n"
|
|
146
|
+
f"## Goal\n"
|
|
147
|
+
f"{goal}\n"
|
|
148
|
+
f"\n"
|
|
149
|
+
f"## Pattern\n"
|
|
150
|
+
f"`{spec.get('pattern', 'sequential')}`\n"
|
|
151
|
+
f"\n"
|
|
152
|
+
f"## Agents\n"
|
|
153
|
+
f"{agents_section}"
|
|
154
|
+
f"## Steps\n"
|
|
155
|
+
f"{steps_section}"
|
|
156
|
+
f"## Policy\n"
|
|
157
|
+
f"- Max turns: {spec.get('policy', {}).get('max_turns', 50)}\n"
|
|
158
|
+
f"- Max time: {spec.get('policy', {}).get('max_total_time_seconds', 3600)}s\n"
|
|
159
|
+
f"\n"
|
|
160
|
+
f"## Risks & Mitigations\n"
|
|
161
|
+
f"- *To be refined with LLM-assisted design.*\n"
|
|
162
|
+
f"\n"
|
|
163
|
+
f"## Execution Instructions\n"
|
|
164
|
+
f"If you are reading this as an Agentic IDE (Cursor, Claude Code, Antigravity, etc.), "
|
|
165
|
+
f"**you are the Orchestrator**.\n\n"
|
|
166
|
+
f"Please execute this loop by fulfilling the roles of each agent described above in sequence, "
|
|
167
|
+
f"writing code and performing tasks as specified in the Steps section.\n"
|
|
168
|
+
f"\n"
|
|
169
|
+
f"---\n"
|
|
170
|
+
f"*Generated by loopengt design (stub mode)*\n"
|
|
171
|
+
)
|