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.
Files changed (87) hide show
  1. loopengt/__init__.py +31 -0
  2. loopengt/adapters/__init__.py +1 -0
  3. loopengt/adapters/antigravity/__init__.py +1 -0
  4. loopengt/adapters/antigravity/adapter.py +55 -0
  5. loopengt/adapters/antigravity/commands.py +21 -0
  6. loopengt/adapters/base.py +51 -0
  7. loopengt/adapters/claude_code/__init__.py +1 -0
  8. loopengt/adapters/claude_code/adapter.py +55 -0
  9. loopengt/adapters/claude_code/commands.py +16 -0
  10. loopengt/adapters/codex/__init__.py +1 -0
  11. loopengt/adapters/codex/adapter.py +52 -0
  12. loopengt/adapters/codex/commands.py +16 -0
  13. loopengt/adapters/cursor/__init__.py +1 -0
  14. loopengt/adapters/cursor/adapter.py +56 -0
  15. loopengt/adapters/cursor/commands.py +29 -0
  16. loopengt/adapters/generic/__init__.py +1 -0
  17. loopengt/adapters/generic/terminal.py +82 -0
  18. loopengt/cli/__init__.py +1 -0
  19. loopengt/cli/commands/__init__.py +1 -0
  20. loopengt/cli/commands/design.py +171 -0
  21. loopengt/cli/commands/doctor.py +110 -0
  22. loopengt/cli/commands/eval.py +105 -0
  23. loopengt/cli/commands/init.py +131 -0
  24. loopengt/cli/commands/mcp_serve.py +57 -0
  25. loopengt/cli/commands/run.py +99 -0
  26. loopengt/cli/commands/template.py +145 -0
  27. loopengt/cli/commands/trace.py +114 -0
  28. loopengt/cli/formatters.py +125 -0
  29. loopengt/cli/main.py +66 -0
  30. loopengt/core/__init__.py +1 -0
  31. loopengt/core/evals/__init__.py +1 -0
  32. loopengt/core/evals/judges.py +216 -0
  33. loopengt/core/evals/metrics.py +119 -0
  34. loopengt/core/evals/regression.py +157 -0
  35. loopengt/core/memory/__init__.py +1 -0
  36. loopengt/core/memory/retrieval.py +124 -0
  37. loopengt/core/memory/store.py +184 -0
  38. loopengt/core/memory/summarizer.py +97 -0
  39. loopengt/core/models/__init__.py +43 -0
  40. loopengt/core/models/agent.py +126 -0
  41. loopengt/core/models/loop_spec.py +251 -0
  42. loopengt/core/models/policy.py +131 -0
  43. loopengt/core/models/state.py +271 -0
  44. loopengt/core/models/tool.py +105 -0
  45. loopengt/core/runtime/__init__.py +1 -0
  46. loopengt/core/runtime/checkpoint.py +152 -0
  47. loopengt/core/runtime/executor.py +463 -0
  48. loopengt/core/runtime/handoff.py +139 -0
  49. loopengt/core/runtime/scheduler.py +168 -0
  50. loopengt/core/tracing/__init__.py +1 -0
  51. loopengt/core/tracing/events.py +95 -0
  52. loopengt/core/tracing/exporters.py +158 -0
  53. loopengt/core/tracing/store.py +202 -0
  54. loopengt/mcp/__init__.py +1 -0
  55. loopengt/mcp/client/__init__.py +1 -0
  56. loopengt/mcp/client/manager.py +118 -0
  57. loopengt/mcp/client/tools.py +107 -0
  58. loopengt/mcp/server/__init__.py +1 -0
  59. loopengt/mcp/server/prompts.py +82 -0
  60. loopengt/mcp/server/resources.py +75 -0
  61. loopengt/mcp/server/server.py +50 -0
  62. loopengt/mcp/server/tools.py +214 -0
  63. loopengt/mcp/shared/__init__.py +1 -0
  64. loopengt/mcp/shared/schemas.py +91 -0
  65. loopengt/plugins/__init__.py +1 -0
  66. loopengt/plugins/base.py +90 -0
  67. loopengt/plugins/loader.py +130 -0
  68. loopengt/plugins/manifest.py +70 -0
  69. loopengt/plugins/registry.py +146 -0
  70. loopengt/prompts/LOOPENGT.md +60 -0
  71. loopengt/prompts/__init__.py +1 -0
  72. loopengt/storage/__init__.py +1 -0
  73. loopengt/storage/jsonl.py +84 -0
  74. loopengt/storage/sqlite.py +102 -0
  75. loopengt/templates/__init__.py +1 -0
  76. loopengt/templates/builtins/handoff_loop/LOOPENGS.md +10 -0
  77. loopengt/templates/builtins/planner_executor/LOOPENGS.md +29 -0
  78. loopengt/templates/builtins/research_architect/LOOPENGS.md +17 -0
  79. loopengt/templates/builtins/reviewer_retry/LOOPENGS.md +29 -0
  80. loopengt/templates/builtins/supervisor_workers/LOOPENGS.md +29 -0
  81. loopengt/templates/loader.py +38 -0
  82. loopengt/templates/registry.py +85 -0
  83. loopengt-0.1.0.dist-info/METADATA +275 -0
  84. loopengt-0.1.0.dist-info/RECORD +87 -0
  85. loopengt-0.1.0.dist-info/WHEEL +4 -0
  86. loopengt-0.1.0.dist-info/entry_points.txt +8 -0
  87. loopengt-0.1.0.dist-info/licenses/LICENSE +674 -0
@@ -0,0 +1,271 @@
1
+ """Runtime state models for loop execution."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import uuid
6
+ from datetime import datetime, timezone
7
+ from enum import StrEnum
8
+ from typing import Any
9
+
10
+ from pydantic import BaseModel, ConfigDict, Field
11
+
12
+
13
+ class RunStatus(StrEnum):
14
+ """Overall status of a loop run."""
15
+
16
+ PENDING = "pending"
17
+ RUNNING = "running"
18
+ PAUSED = "paused"
19
+ COMPLETED = "completed"
20
+ FAILED = "failed"
21
+ CANCELLED = "cancelled"
22
+
23
+
24
+ class StepStatus(StrEnum):
25
+ """Status of a single step execution."""
26
+
27
+ PENDING = "pending"
28
+ RUNNING = "running"
29
+ COMPLETED = "completed"
30
+ FAILED = "failed"
31
+ SKIPPED = "skipped"
32
+ RETRYING = "retrying"
33
+
34
+
35
+ def _utc_now() -> datetime:
36
+ """Return the current UTC timestamp."""
37
+ return datetime.now(timezone.utc)
38
+
39
+
40
+ def _new_id() -> str:
41
+ """Generate a new unique run/step ID."""
42
+ return uuid.uuid4().hex[:12]
43
+
44
+
45
+ class StepResult(BaseModel):
46
+ """The output produced by a single step execution."""
47
+
48
+ model_config = ConfigDict(frozen=True)
49
+
50
+ output: Any = Field(default=None, description="Step output data")
51
+ error: str | None = Field(default=None, description="Error message if failed")
52
+ duration_seconds: float = Field(
53
+ default=0.0, ge=0, description="Wall-clock execution time"
54
+ )
55
+ token_usage: dict[str, int] = Field(
56
+ default_factory=dict,
57
+ description="Token usage breakdown (prompt_tokens, completion_tokens, …)",
58
+ )
59
+ metadata: dict[str, Any] = Field(
60
+ default_factory=dict, description="Arbitrary result metadata"
61
+ )
62
+
63
+
64
+ class StepState(BaseModel):
65
+ """Mutable runtime state for a single step."""
66
+
67
+ model_config = ConfigDict()
68
+
69
+ step_name: str = Field(..., description="Name of the step")
70
+ status: StepStatus = Field(
71
+ default=StepStatus.PENDING, description="Current status"
72
+ )
73
+ attempts: int = Field(default=0, ge=0, description="Number of attempts so far")
74
+ result: StepResult | None = Field(
75
+ default=None, description="Most recent execution result"
76
+ )
77
+ started_at: datetime | None = Field(
78
+ default=None, description="When execution began"
79
+ )
80
+ completed_at: datetime | None = Field(
81
+ default=None, description="When execution finished"
82
+ )
83
+
84
+ def mark_running(self) -> None:
85
+ """Transition to running status."""
86
+ self.status = StepStatus.RUNNING
87
+ self.attempts += 1
88
+ self.started_at = _utc_now()
89
+ self.completed_at = None
90
+
91
+ def mark_completed(self, result: StepResult) -> None:
92
+ """Transition to completed status with the given result."""
93
+ self.status = StepStatus.COMPLETED
94
+ self.result = result
95
+ self.completed_at = _utc_now()
96
+
97
+ def mark_failed(self, error: str) -> None:
98
+ """Transition to failed status."""
99
+ self.status = StepStatus.FAILED
100
+ self.result = StepResult(error=error)
101
+ self.completed_at = _utc_now()
102
+
103
+ def mark_retrying(self) -> None:
104
+ """Transition to retrying status."""
105
+ self.status = StepStatus.RETRYING
106
+
107
+
108
+ class AgentState(BaseModel):
109
+ """Mutable runtime state for a single agent across the loop."""
110
+
111
+ model_config = ConfigDict()
112
+
113
+ agent_name: str = Field(..., description="Name of the agent")
114
+ invocations: int = Field(
115
+ default=0, ge=0, description="Total invocations in this run"
116
+ )
117
+ total_tokens: int = Field(
118
+ default=0, ge=0, description="Cumulative token usage"
119
+ )
120
+ errors: int = Field(
121
+ default=0, ge=0, description="Total errors encountered"
122
+ )
123
+ last_output: Any = Field(
124
+ default=None, description="Most recent output from this agent"
125
+ )
126
+ context: dict[str, Any] = Field(
127
+ default_factory=dict,
128
+ description="Agent-local context / scratchpad",
129
+ )
130
+
131
+ def record_invocation(
132
+ self, tokens: int = 0, *, error: bool = False
133
+ ) -> None:
134
+ """Record one invocation, updating counters."""
135
+ self.invocations += 1
136
+ self.total_tokens += tokens
137
+ if error:
138
+ self.errors += 1
139
+
140
+
141
+ class CheckpointData(BaseModel):
142
+ """Serializable snapshot of loop state for persistence and resumption."""
143
+
144
+ model_config = ConfigDict(frozen=True)
145
+
146
+ checkpoint_id: str = Field(
147
+ default_factory=_new_id, description="Unique checkpoint ID"
148
+ )
149
+ run_id: str = Field(..., description="Parent run ID")
150
+ created_at: datetime = Field(
151
+ default_factory=_utc_now, description="Creation timestamp"
152
+ )
153
+ current_step_index: int = Field(
154
+ ..., ge=0, description="Index of the step being executed"
155
+ )
156
+ step_states: list[StepState] = Field(
157
+ default_factory=list, description="Snapshot of all step states"
158
+ )
159
+ agent_states: list[AgentState] = Field(
160
+ default_factory=list, description="Snapshot of all agent states"
161
+ )
162
+ context: dict[str, Any] = Field(
163
+ default_factory=dict, description="Full loop context at checkpoint time"
164
+ )
165
+ history: list[dict[str, Any]] = Field(
166
+ default_factory=list,
167
+ description="Ordered history of completed step outputs",
168
+ )
169
+
170
+
171
+ class LoopState(BaseModel):
172
+ """Mutable runtime state for an entire loop run.
173
+
174
+ Created by the executor when a run starts. Updated in-place as steps
175
+ execute. Can be checkpointed and restored for pause/resume.
176
+ """
177
+
178
+ model_config = ConfigDict()
179
+
180
+ run_id: str = Field(
181
+ default_factory=_new_id, description="Unique run identifier"
182
+ )
183
+ loop_name: str = Field(..., description="Name of the loop spec being executed")
184
+ status: RunStatus = Field(
185
+ default=RunStatus.PENDING, description="Overall run status"
186
+ )
187
+ current_step_index: int = Field(
188
+ default=0, ge=0, description="Index of the step currently being executed"
189
+ )
190
+ turn: int = Field(
191
+ default=0, ge=0, description="Current turn number (across all agents)"
192
+ )
193
+ step_states: dict[str, StepState] = Field(
194
+ default_factory=dict,
195
+ description="Map of step name → StepState",
196
+ )
197
+ agent_states: dict[str, AgentState] = Field(
198
+ default_factory=dict,
199
+ description="Map of agent name → AgentState",
200
+ )
201
+ context: dict[str, Any] = Field(
202
+ default_factory=dict,
203
+ description="Shared mutable context accessible to all agents",
204
+ )
205
+ history: list[dict[str, Any]] = Field(
206
+ default_factory=list,
207
+ description="Ordered history of step results",
208
+ )
209
+ started_at: datetime = Field(
210
+ default_factory=_utc_now, description="Run start timestamp"
211
+ )
212
+ completed_at: datetime | None = Field(
213
+ default=None, description="Run completion timestamp"
214
+ )
215
+ error: str | None = Field(
216
+ default=None, description="Top-level error message if run failed"
217
+ )
218
+ checkpoints: list[str] = Field(
219
+ default_factory=list,
220
+ description="List of checkpoint IDs created during this run",
221
+ )
222
+
223
+ # ------------------------------------------------------------------
224
+ # Lifecycle helpers
225
+ # ------------------------------------------------------------------
226
+
227
+ def mark_running(self) -> None:
228
+ """Transition to running status."""
229
+ self.status = RunStatus.RUNNING
230
+
231
+ def mark_completed(self) -> None:
232
+ """Transition to completed status."""
233
+ self.status = RunStatus.COMPLETED
234
+ self.completed_at = _utc_now()
235
+
236
+ def mark_failed(self, error: str) -> None:
237
+ """Transition to failed status with an error message."""
238
+ self.status = RunStatus.FAILED
239
+ self.error = error
240
+ self.completed_at = _utc_now()
241
+
242
+ def mark_cancelled(self) -> None:
243
+ """Transition to cancelled status."""
244
+ self.status = RunStatus.CANCELLED
245
+ self.completed_at = _utc_now()
246
+
247
+ def increment_turn(self) -> None:
248
+ """Advance the turn counter."""
249
+ self.turn += 1
250
+
251
+ def add_to_history(self, step_name: str, result: Any) -> None:
252
+ """Append a step result to the history."""
253
+ self.history.append(
254
+ {
255
+ "step": step_name,
256
+ "turn": self.turn,
257
+ "result": result,
258
+ "timestamp": _utc_now().isoformat(),
259
+ }
260
+ )
261
+
262
+ def to_checkpoint(self) -> CheckpointData:
263
+ """Create a checkpoint snapshot of the current state."""
264
+ return CheckpointData(
265
+ run_id=self.run_id,
266
+ current_step_index=self.current_step_index,
267
+ step_states=list(self.step_states.values()),
268
+ agent_states=list(self.agent_states.values()),
269
+ context=self.context.copy(),
270
+ history=list(self.history),
271
+ )
@@ -0,0 +1,105 @@
1
+ """Tool and MCP capability definitions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from enum import StrEnum
6
+ from typing import Any
7
+
8
+ from pydantic import BaseModel, ConfigDict, Field
9
+
10
+
11
+ class ToolType(StrEnum):
12
+ """Classification of tool integration type."""
13
+
14
+ MCP = "mcp"
15
+ FUNCTION = "function"
16
+ BUILTIN = "builtin"
17
+
18
+
19
+ class ToolAuth(BaseModel):
20
+ """Authentication configuration for a tool.
21
+
22
+ Supports API key, OAuth, and bearer token patterns. Secrets are
23
+ referenced by environment-variable name — never stored in plain text.
24
+ """
25
+
26
+ model_config = ConfigDict(frozen=True)
27
+
28
+ method: str = Field(
29
+ default="none",
30
+ description="Auth method: none, api_key, bearer, oauth",
31
+ )
32
+ env_var: str | None = Field(
33
+ default=None,
34
+ description="Environment variable holding the secret",
35
+ )
36
+ header_name: str = Field(
37
+ default="Authorization",
38
+ description="HTTP header used to pass the credential",
39
+ )
40
+ extra: dict[str, Any] = Field(
41
+ default_factory=dict,
42
+ description="Additional auth parameters",
43
+ )
44
+
45
+
46
+ class ToolSpec(BaseModel):
47
+ """Specification for a tool available to agents in a loop.
48
+
49
+ Tools can be MCP server tools, local Python functions, or built-in
50
+ framework utilities.
51
+ """
52
+
53
+ model_config = ConfigDict(frozen=True)
54
+
55
+ name: str = Field(
56
+ ..., min_length=1, max_length=128, description="Unique tool identifier"
57
+ )
58
+ description: str = Field(
59
+ default="",
60
+ max_length=2048,
61
+ description="Human-readable description of what the tool does",
62
+ )
63
+ tool_type: ToolType = Field(
64
+ default=ToolType.FUNCTION,
65
+ description="Integration type: mcp, function, or builtin",
66
+ )
67
+ schema_def: dict[str, Any] = Field(
68
+ default_factory=dict,
69
+ description="JSON Schema for the tool's input parameters",
70
+ )
71
+ return_schema: dict[str, Any] | None = Field(
72
+ default=None,
73
+ description="JSON Schema for the tool's return value",
74
+ )
75
+ auth: ToolAuth = Field(
76
+ default_factory=ToolAuth,
77
+ description="Authentication configuration",
78
+ )
79
+ timeout_seconds: float = Field(
80
+ default=60.0, gt=0, description="Maximum execution time"
81
+ )
82
+ mcp_server: str | None = Field(
83
+ default=None,
84
+ description="MCP server name (required when tool_type is 'mcp')",
85
+ )
86
+ function_ref: str | None = Field(
87
+ default=None,
88
+ description="Dotted import path for function tools (e.g. 'mymod.utils.do_thing')",
89
+ )
90
+ metadata: dict[str, Any] = Field(
91
+ default_factory=dict,
92
+ description="Arbitrary key-value metadata",
93
+ )
94
+
95
+ def is_mcp(self) -> bool:
96
+ """Return True if this is an MCP-backed tool."""
97
+ return self.tool_type == ToolType.MCP
98
+
99
+ def is_function(self) -> bool:
100
+ """Return True if this is a local function tool."""
101
+ return self.tool_type == ToolType.FUNCTION
102
+
103
+ def is_builtin(self) -> bool:
104
+ """Return True if this is a framework built-in tool."""
105
+ return self.tool_type == ToolType.BUILTIN
@@ -0,0 +1 @@
1
+ """Runtime engine for loop execution."""
@@ -0,0 +1,152 @@
1
+ """State persistence via checkpoints for pause/resume support."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ import structlog
10
+
11
+ from loopengt.core.models.state import CheckpointData, LoopState
12
+
13
+ logger = structlog.get_logger(__name__)
14
+
15
+
16
+ class CheckpointError(Exception):
17
+ """Raised when a checkpoint operation fails."""
18
+
19
+
20
+ class CheckpointManager:
21
+ """SQLite-backed (or file-backed) state persistence for loop resumption.
22
+
23
+ In the current implementation, checkpoints are stored as JSON files
24
+ under a runs directory. A future version will use SQLite for better
25
+ query and concurrency support.
26
+
27
+ Usage::
28
+
29
+ mgr = CheckpointManager(runs_dir=Path(".loopengt/runs"))
30
+ cp_id = await mgr.save(state)
31
+ restored = await mgr.load(run_id, cp_id)
32
+ """
33
+
34
+ def __init__(self, runs_dir: Path | None = None) -> None:
35
+ self._runs_dir = runs_dir or Path(".loopengt/runs")
36
+ self._runs_dir.mkdir(parents=True, exist_ok=True)
37
+ self._log = logger.bind(component="checkpoint")
38
+
39
+ # ------------------------------------------------------------------
40
+ # Public API
41
+ # ------------------------------------------------------------------
42
+
43
+ async def save(self, state: LoopState) -> str:
44
+ """Create a checkpoint from the current loop state.
45
+
46
+ Returns the checkpoint ID.
47
+ """
48
+ checkpoint = state.to_checkpoint()
49
+ state.checkpoints.append(checkpoint.checkpoint_id)
50
+
51
+ cp_path = self._checkpoint_path(state.run_id, checkpoint.checkpoint_id)
52
+ cp_path.parent.mkdir(parents=True, exist_ok=True)
53
+
54
+ data = checkpoint.model_dump(mode="json")
55
+ cp_path.write_text(
56
+ json.dumps(data, indent=2, default=str), encoding="utf-8"
57
+ )
58
+
59
+ self._log.info(
60
+ "checkpoint.saved",
61
+ run_id=state.run_id,
62
+ checkpoint_id=checkpoint.checkpoint_id,
63
+ )
64
+ return checkpoint.checkpoint_id
65
+
66
+ async def load(
67
+ self, run_id: str, checkpoint_id: str | None = None
68
+ ) -> CheckpointData:
69
+ """Load a checkpoint.
70
+
71
+ If *checkpoint_id* is ``None``, loads the latest checkpoint for
72
+ the given run.
73
+ """
74
+ if checkpoint_id:
75
+ cp_path = self._checkpoint_path(run_id, checkpoint_id)
76
+ else:
77
+ cp_path = self._latest_checkpoint_path(run_id)
78
+
79
+ if not cp_path.exists():
80
+ raise CheckpointError(
81
+ f"Checkpoint not found: {cp_path}"
82
+ )
83
+
84
+ data = json.loads(cp_path.read_text(encoding="utf-8"))
85
+ checkpoint = CheckpointData.model_validate(data)
86
+
87
+ self._log.info(
88
+ "checkpoint.loaded",
89
+ run_id=run_id,
90
+ checkpoint_id=checkpoint.checkpoint_id,
91
+ )
92
+ return checkpoint
93
+
94
+ async def list_checkpoints(self, run_id: str) -> list[str]:
95
+ """List checkpoint IDs for a given run."""
96
+ cp_dir = self._runs_dir / run_id / "checkpoints"
97
+ if not cp_dir.exists():
98
+ return []
99
+
100
+ return sorted(
101
+ p.stem for p in cp_dir.glob("*.json")
102
+ )
103
+
104
+ async def delete(self, run_id: str, checkpoint_id: str) -> None:
105
+ """Delete a specific checkpoint."""
106
+ cp_path = self._checkpoint_path(run_id, checkpoint_id)
107
+ if cp_path.exists():
108
+ cp_path.unlink()
109
+ self._log.info(
110
+ "checkpoint.deleted",
111
+ run_id=run_id,
112
+ checkpoint_id=checkpoint_id,
113
+ )
114
+
115
+ def restore_state(
116
+ self, checkpoint: CheckpointData, base_state: LoopState
117
+ ) -> LoopState:
118
+ """Apply checkpoint data to a LoopState, restoring it.
119
+
120
+ Returns the restored state (mutates *base_state* in-place).
121
+ """
122
+ base_state.current_step_index = checkpoint.current_step_index
123
+ base_state.context = dict(checkpoint.context)
124
+ base_state.history = list(checkpoint.history)
125
+
126
+ for ss in checkpoint.step_states:
127
+ base_state.step_states[ss.step_name] = ss
128
+
129
+ for ags in checkpoint.agent_states:
130
+ base_state.agent_states[ags.agent_name] = ags
131
+
132
+ return base_state
133
+
134
+ # ------------------------------------------------------------------
135
+ # Internals
136
+ # ------------------------------------------------------------------
137
+
138
+ def _checkpoint_path(self, run_id: str, checkpoint_id: str) -> Path:
139
+ """Return the file path for a specific checkpoint."""
140
+ return self._runs_dir / run_id / "checkpoints" / f"{checkpoint_id}.json"
141
+
142
+ def _latest_checkpoint_path(self, run_id: str) -> Path:
143
+ """Return the path of the most recent checkpoint for a run."""
144
+ cp_dir = self._runs_dir / run_id / "checkpoints"
145
+ if not cp_dir.exists():
146
+ raise CheckpointError(f"No checkpoints for run {run_id}")
147
+
148
+ candidates = sorted(cp_dir.glob("*.json"), key=lambda p: p.stat().st_mtime)
149
+ if not candidates:
150
+ raise CheckpointError(f"No checkpoints for run {run_id}")
151
+
152
+ return candidates[-1]