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,157 @@
1
+ """Regression test harness for loop specifications."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import json
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ import structlog
11
+
12
+ from loopengt.core.evals.metrics import MetricResult, evaluate_all
13
+
14
+ logger = structlog.get_logger(__name__)
15
+
16
+
17
+ class RegressionResult:
18
+ """Result of a regression test run."""
19
+
20
+ __slots__ = ("spec_name", "baseline_metrics", "current_metrics", "regressions")
21
+
22
+ def __init__(
23
+ self,
24
+ spec_name: str,
25
+ baseline_metrics: list[MetricResult],
26
+ current_metrics: list[MetricResult],
27
+ regressions: list[str],
28
+ ) -> None:
29
+ self.spec_name = spec_name
30
+ self.baseline_metrics = baseline_metrics
31
+ self.current_metrics = current_metrics
32
+ self.regressions = regressions
33
+
34
+ @property
35
+ def passed(self) -> bool:
36
+ return len(self.regressions) == 0
37
+
38
+ def to_dict(self) -> dict[str, Any]:
39
+ return {
40
+ "spec_name": self.spec_name,
41
+ "passed": self.passed,
42
+ "regressions": self.regressions,
43
+ "baseline": [m.to_dict() for m in self.baseline_metrics],
44
+ "current": [m.to_dict() for m in self.current_metrics],
45
+ }
46
+
47
+
48
+ class RegressionHarness:
49
+ """Runs loop specs and compares against baseline metrics.
50
+
51
+ Usage::
52
+
53
+ harness = RegressionHarness(baselines_dir=Path(".loopengt/baselines"))
54
+ result = await harness.run(spec_path, baseline_id="v1.0")
55
+ """
56
+
57
+ def __init__(self, baselines_dir: Path | None = None) -> None:
58
+ self._baselines_dir = baselines_dir or Path(".loopengt/baselines")
59
+ self._baselines_dir.mkdir(parents=True, exist_ok=True)
60
+ self._log = logger.bind(component="regression")
61
+
62
+ async def run(
63
+ self,
64
+ spec_path: Path,
65
+ baseline_id: str | None = None,
66
+ ) -> RegressionResult:
67
+ """Execute a spec and compare with baseline metrics."""
68
+ import yaml
69
+
70
+ from loopengt.core.models.loop_spec import LoopSpec
71
+ from loopengt.core.runtime.executor import LoopExecutor
72
+
73
+ # Load and run the spec
74
+ raw = yaml.safe_load(spec_path.read_text(encoding="utf-8"))
75
+ spec = LoopSpec.model_validate(raw)
76
+ executor = LoopExecutor(spec)
77
+ state = await executor.execute()
78
+
79
+ # Evaluate current run
80
+ trace = state.model_dump(mode="json")
81
+ current_metrics = evaluate_all(trace)
82
+
83
+ # Load baseline
84
+ baseline_metrics = self._load_baseline(spec.name, baseline_id)
85
+
86
+ # Compare
87
+ regressions = self._compare(baseline_metrics, current_metrics)
88
+
89
+ result = RegressionResult(
90
+ spec_name=spec.name,
91
+ baseline_metrics=baseline_metrics,
92
+ current_metrics=current_metrics,
93
+ regressions=regressions,
94
+ )
95
+
96
+ self._log.info(
97
+ "regression.complete",
98
+ spec=spec.name,
99
+ passed=result.passed,
100
+ regressions=len(regressions),
101
+ )
102
+
103
+ return result
104
+
105
+ async def save_baseline(
106
+ self, spec_name: str, metrics: list[MetricResult], baseline_id: str = "latest"
107
+ ) -> None:
108
+ """Save metrics as a baseline for future comparison."""
109
+ path = self._baselines_dir / f"{spec_name}_{baseline_id}.json"
110
+ data = [m.to_dict() for m in metrics]
111
+ path.write_text(json.dumps(data, indent=2), encoding="utf-8")
112
+ self._log.info("regression.baseline_saved", spec=spec_name, id=baseline_id)
113
+
114
+ def _load_baseline(
115
+ self, spec_name: str, baseline_id: str | None
116
+ ) -> list[MetricResult]:
117
+ """Load baseline metrics. Returns empty list if none found."""
118
+ bid = baseline_id or "latest"
119
+ path = self._baselines_dir / f"{spec_name}_{bid}.json"
120
+
121
+ if not path.exists():
122
+ return []
123
+
124
+ data = json.loads(path.read_text(encoding="utf-8"))
125
+ return [
126
+ MetricResult(
127
+ name=m["name"],
128
+ value=m["value"],
129
+ passed=m["passed"],
130
+ detail=m.get("detail", ""),
131
+ )
132
+ for m in data
133
+ ]
134
+
135
+ def _compare(
136
+ self, baseline: list[MetricResult], current: list[MetricResult]
137
+ ) -> list[str]:
138
+ """Compare current metrics against baseline. Returns regression descriptions."""
139
+ if not baseline:
140
+ return [] # No baseline to compare against
141
+
142
+ baseline_map = {m.name: m for m in baseline}
143
+ regressions = []
144
+
145
+ for metric in current:
146
+ base = baseline_map.get(metric.name)
147
+ if base is None:
148
+ continue
149
+
150
+ # Check for regressions
151
+ if base.passed and not metric.passed:
152
+ regressions.append(
153
+ f"{metric.name}: was passing, now failing "
154
+ f"(baseline={base.value}, current={metric.value})"
155
+ )
156
+
157
+ return regressions
@@ -0,0 +1 @@
1
+ """Memory subsystem for agent context management."""
@@ -0,0 +1,124 @@
1
+ """Retrieval logic for agent memory — keyword + semantic."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ import structlog
8
+
9
+ from loopengt.core.memory.store import MemoryEntry, MemoryStore
10
+
11
+ logger = structlog.get_logger(__name__)
12
+
13
+
14
+ class MemoryRetriever:
15
+ """Retrieves relevant memories using keyword and/or vector search.
16
+
17
+ Combines results from both search strategies with configurable
18
+ weighting.
19
+
20
+ Usage::
21
+
22
+ retriever = MemoryRetriever(store)
23
+ results = retriever.retrieve("API design patterns", top_k=5)
24
+ """
25
+
26
+ def __init__(
27
+ self,
28
+ store: MemoryStore,
29
+ *,
30
+ keyword_weight: float = 0.5,
31
+ vector_weight: float = 0.5,
32
+ ) -> None:
33
+ self._store = store
34
+ self._keyword_weight = keyword_weight
35
+ self._vector_weight = vector_weight
36
+ self._log = logger.bind(component="retriever")
37
+
38
+ def retrieve(
39
+ self,
40
+ query: str,
41
+ *,
42
+ top_k: int = 5,
43
+ metadata_filter: dict[str, Any] | None = None,
44
+ query_embedding: list[float] | None = None,
45
+ ) -> list[MemoryEntry]:
46
+ """Retrieve top-k relevant memories.
47
+
48
+ If *query_embedding* is provided, combines keyword and vector
49
+ search scores. Otherwise, uses keyword search only.
50
+ """
51
+ keyword_results = self._store.search(
52
+ query, top_k=top_k * 2, metadata_filter=metadata_filter
53
+ )
54
+
55
+ if query_embedding is None:
56
+ return keyword_results[:top_k]
57
+
58
+ vector_results = self._store.vector_search(
59
+ query_embedding, top_k=top_k * 2, metadata_filter=metadata_filter
60
+ )
61
+
62
+ # Merge and re-rank
63
+ merged = self._merge_results(keyword_results, vector_results)
64
+ return merged[:top_k]
65
+
66
+ def retrieve_by_metadata(
67
+ self,
68
+ metadata_filter: dict[str, Any],
69
+ *,
70
+ top_k: int = 10,
71
+ ) -> list[MemoryEntry]:
72
+ """Retrieve entries matching specific metadata criteria."""
73
+ results = [
74
+ entry
75
+ for entry in self._store.all_entries()
76
+ if all(entry.metadata.get(k) == v for k, v in metadata_filter.items())
77
+ ]
78
+ return results[:top_k]
79
+
80
+ def retrieve_recent(self, *, top_k: int = 5) -> list[MemoryEntry]:
81
+ """Retrieve the most recently added entries."""
82
+ entries = self._store.all_entries()
83
+ # Entries are stored in insertion order (dict in Python 3.7+)
84
+ return entries[-top_k:][::-1]
85
+
86
+ # ------------------------------------------------------------------
87
+ # Internals
88
+ # ------------------------------------------------------------------
89
+
90
+ def _merge_results(
91
+ self,
92
+ keyword: list[MemoryEntry],
93
+ vector: list[MemoryEntry],
94
+ ) -> list[MemoryEntry]:
95
+ """Merge keyword and vector results with weighted scoring."""
96
+ combined: dict[str, MemoryEntry] = {}
97
+
98
+ for entry in keyword:
99
+ eid = entry.entry_id
100
+ if eid not in combined:
101
+ combined[eid] = MemoryEntry(
102
+ entry_id=eid,
103
+ content=entry.content,
104
+ metadata=entry.metadata,
105
+ embedding=entry.embedding,
106
+ score=0.0,
107
+ )
108
+ combined[eid].score += entry.score * self._keyword_weight
109
+
110
+ for entry in vector:
111
+ eid = entry.entry_id
112
+ if eid not in combined:
113
+ combined[eid] = MemoryEntry(
114
+ entry_id=eid,
115
+ content=entry.content,
116
+ metadata=entry.metadata,
117
+ embedding=entry.embedding,
118
+ score=0.0,
119
+ )
120
+ combined[eid].score += entry.score * self._vector_weight
121
+
122
+ results = list(combined.values())
123
+ results.sort(key=lambda e: e.score, reverse=True)
124
+ return results
@@ -0,0 +1,184 @@
1
+ """In-memory vector + keyword store for agent memory."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import math
7
+ from dataclasses import dataclass, field
8
+ from typing import Any
9
+
10
+ import structlog
11
+
12
+ logger = structlog.get_logger(__name__)
13
+
14
+
15
+ @dataclass
16
+ class MemoryEntry:
17
+ """A single memory entry with text content and optional embedding."""
18
+
19
+ entry_id: str
20
+ content: str
21
+ metadata: dict[str, Any] = field(default_factory=dict)
22
+ embedding: list[float] | None = None
23
+ score: float = 0.0
24
+
25
+
26
+ class MemoryStore:
27
+ """Lightweight in-memory store for agent memories.
28
+
29
+ Supports keyword search and (when embeddings are provided)
30
+ cosine-similarity based vector search.
31
+
32
+ Usage::
33
+
34
+ store = MemoryStore()
35
+ store.add("plan-1", "Build the API endpoints first", metadata={"step": "plan"})
36
+ results = store.search("API endpoints", top_k=3)
37
+ """
38
+
39
+ def __init__(self) -> None:
40
+ self._entries: dict[str, MemoryEntry] = {}
41
+ self._log = logger.bind(component="memory_store")
42
+
43
+ # ------------------------------------------------------------------
44
+ # Write operations
45
+ # ------------------------------------------------------------------
46
+
47
+ def add(
48
+ self,
49
+ entry_id: str | None = None,
50
+ content: str = "",
51
+ metadata: dict[str, Any] | None = None,
52
+ embedding: list[float] | None = None,
53
+ ) -> str:
54
+ """Add a memory entry. Returns the entry ID."""
55
+ if entry_id is None:
56
+ entry_id = hashlib.sha256(content.encode()).hexdigest()[:12]
57
+
58
+ self._entries[entry_id] = MemoryEntry(
59
+ entry_id=entry_id,
60
+ content=content,
61
+ metadata=metadata or {},
62
+ embedding=embedding,
63
+ )
64
+ self._log.debug("memory.add", entry_id=entry_id, length=len(content))
65
+ return entry_id
66
+
67
+ def remove(self, entry_id: str) -> bool:
68
+ """Remove an entry by ID. Returns True if found."""
69
+ if entry_id in self._entries:
70
+ del self._entries[entry_id]
71
+ return True
72
+ return False
73
+
74
+ def clear(self) -> None:
75
+ """Remove all entries."""
76
+ self._entries.clear()
77
+
78
+ # ------------------------------------------------------------------
79
+ # Read / search operations
80
+ # ------------------------------------------------------------------
81
+
82
+ def get(self, entry_id: str) -> MemoryEntry | None:
83
+ """Get an entry by ID."""
84
+ return self._entries.get(entry_id)
85
+
86
+ def search(
87
+ self,
88
+ query: str,
89
+ *,
90
+ top_k: int = 5,
91
+ metadata_filter: dict[str, Any] | None = None,
92
+ ) -> list[MemoryEntry]:
93
+ """Search entries by keyword matching.
94
+
95
+ Scores entries by the fraction of query words found in the
96
+ content. Optionally filters by metadata key-value matches.
97
+ """
98
+ query_words = set(query.lower().split())
99
+ if not query_words:
100
+ return []
101
+
102
+ scored: list[MemoryEntry] = []
103
+ for entry in self._entries.values():
104
+ if metadata_filter and not self._matches_filter(entry, metadata_filter):
105
+ continue
106
+
107
+ content_words = set(entry.content.lower().split())
108
+ overlap = query_words & content_words
109
+ score = len(overlap) / len(query_words) if query_words else 0.0
110
+
111
+ if score > 0:
112
+ # Return a copy with the score
113
+ scored.append(
114
+ MemoryEntry(
115
+ entry_id=entry.entry_id,
116
+ content=entry.content,
117
+ metadata=entry.metadata,
118
+ embedding=entry.embedding,
119
+ score=score,
120
+ )
121
+ )
122
+
123
+ scored.sort(key=lambda e: e.score, reverse=True)
124
+ return scored[:top_k]
125
+
126
+ def vector_search(
127
+ self,
128
+ query_embedding: list[float],
129
+ *,
130
+ top_k: int = 5,
131
+ metadata_filter: dict[str, Any] | None = None,
132
+ ) -> list[MemoryEntry]:
133
+ """Search entries by cosine similarity of embeddings."""
134
+ scored: list[MemoryEntry] = []
135
+
136
+ for entry in self._entries.values():
137
+ if entry.embedding is None:
138
+ continue
139
+ if metadata_filter and not self._matches_filter(entry, metadata_filter):
140
+ continue
141
+
142
+ sim = self._cosine_similarity(query_embedding, entry.embedding)
143
+ scored.append(
144
+ MemoryEntry(
145
+ entry_id=entry.entry_id,
146
+ content=entry.content,
147
+ metadata=entry.metadata,
148
+ embedding=entry.embedding,
149
+ score=sim,
150
+ )
151
+ )
152
+
153
+ scored.sort(key=lambda e: e.score, reverse=True)
154
+ return scored[:top_k]
155
+
156
+ @property
157
+ def size(self) -> int:
158
+ """Number of entries in the store."""
159
+ return len(self._entries)
160
+
161
+ def all_entries(self) -> list[MemoryEntry]:
162
+ """Return all entries."""
163
+ return list(self._entries.values())
164
+
165
+ # ------------------------------------------------------------------
166
+ # Internals
167
+ # ------------------------------------------------------------------
168
+
169
+ @staticmethod
170
+ def _cosine_similarity(a: list[float], b: list[float]) -> float:
171
+ """Compute cosine similarity between two vectors."""
172
+ if len(a) != len(b):
173
+ return 0.0
174
+ dot = sum(x * y for x, y in zip(a, b))
175
+ norm_a = math.sqrt(sum(x * x for x in a))
176
+ norm_b = math.sqrt(sum(x * x for x in b))
177
+ if norm_a == 0 or norm_b == 0:
178
+ return 0.0
179
+ return dot / (norm_a * norm_b)
180
+
181
+ @staticmethod
182
+ def _matches_filter(entry: MemoryEntry, filt: dict[str, Any]) -> bool:
183
+ """Check if an entry's metadata matches all filter conditions."""
184
+ return all(entry.metadata.get(k) == v for k, v in filt.items())
@@ -0,0 +1,97 @@
1
+ """Context window summarization for long-running loops."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import structlog
6
+
7
+ logger = structlog.get_logger(__name__)
8
+
9
+
10
+ class Summarizer:
11
+ """Summarises conversation / context history to fit within token budgets.
12
+
13
+ Uses a simple extractive strategy by default. When an LLM provider
14
+ is available, switches to abstractive summarization.
15
+
16
+ Usage::
17
+
18
+ summarizer = Summarizer(max_tokens=4096)
19
+ short = summarizer.summarize(long_history)
20
+ """
21
+
22
+ def __init__(
23
+ self,
24
+ *,
25
+ max_tokens: int = 4096,
26
+ strategy: str = "extractive",
27
+ ) -> None:
28
+ self._max_tokens = max_tokens
29
+ self._strategy = strategy
30
+ self._log = logger.bind(component="summarizer")
31
+
32
+ def summarize(self, entries: list[str]) -> str:
33
+ """Produce a condensed summary of the given text entries.
34
+
35
+ Parameters
36
+ ----------
37
+ entries:
38
+ List of text chunks (e.g., step outputs, conversation turns)
39
+ to summarize.
40
+
41
+ Returns
42
+ -------
43
+ str
44
+ The summarized text, estimated to be within ``max_tokens``.
45
+ """
46
+ if not entries:
47
+ return ""
48
+
49
+ full_text = "\n\n".join(entries)
50
+ estimated_tokens = len(full_text.split()) # rough word-based estimate
51
+
52
+ if estimated_tokens <= self._max_tokens:
53
+ return full_text
54
+
55
+ self._log.info(
56
+ "summarizer.truncating",
57
+ original_tokens=estimated_tokens,
58
+ target_tokens=self._max_tokens,
59
+ )
60
+
61
+ if self._strategy == "extractive":
62
+ return self._extractive_summarize(entries)
63
+ else:
64
+ return self._truncate(full_text)
65
+
66
+ def _extractive_summarize(self, entries: list[str]) -> str:
67
+ """Keep the first and last entries, plus sampled middle entries."""
68
+ if len(entries) <= 3:
69
+ return "\n\n".join(entries)
70
+
71
+ # Always keep first and last
72
+ selected = [entries[0]]
73
+
74
+ # Sample from the middle, keeping budget
75
+ middle = entries[1:-1]
76
+ budget = self._max_tokens - len(entries[0].split()) - len(entries[-1].split())
77
+
78
+ used = 0
79
+ step = max(1, len(middle) // 5) # keep ~5 middle entries
80
+ for i in range(0, len(middle), step):
81
+ entry_tokens = len(middle[i].split())
82
+ if used + entry_tokens > budget:
83
+ selected.append("... [context truncated] ...")
84
+ break
85
+ selected.append(middle[i])
86
+ used += entry_tokens
87
+
88
+ selected.append(entries[-1])
89
+ return "\n\n".join(selected)
90
+
91
+ def _truncate(self, text: str) -> str:
92
+ """Simple truncation to fit token budget."""
93
+ words = text.split()
94
+ if len(words) <= self._max_tokens:
95
+ return text
96
+ truncated = " ".join(words[: self._max_tokens])
97
+ return truncated + "\n\n... [truncated]"
@@ -0,0 +1,43 @@
1
+ """Core data models for loop engineering."""
2
+
3
+ from loopengt.core.models.agent import AgentCapability, AgentConfig, AgentRole
4
+ from loopengt.core.models.loop_spec import (
5
+ LoopSpec,
6
+ OutputSchema,
7
+ StepSpec,
8
+ StopCondition,
9
+ )
10
+ from loopengt.core.models.policy import (
11
+ BackoffStrategy,
12
+ Policy,
13
+ RetryPolicy,
14
+ VerificationGate,
15
+ )
16
+ from loopengt.core.models.state import (
17
+ AgentState,
18
+ CheckpointData,
19
+ LoopState,
20
+ StepState,
21
+ )
22
+ from loopengt.core.models.tool import ToolAuth, ToolSpec, ToolType
23
+
24
+ __all__ = [
25
+ "AgentCapability",
26
+ "AgentConfig",
27
+ "AgentRole",
28
+ "AgentState",
29
+ "BackoffStrategy",
30
+ "CheckpointData",
31
+ "LoopSpec",
32
+ "LoopState",
33
+ "OutputSchema",
34
+ "Policy",
35
+ "RetryPolicy",
36
+ "StepSpec",
37
+ "StepState",
38
+ "StopCondition",
39
+ "ToolAuth",
40
+ "ToolSpec",
41
+ "ToolType",
42
+ "VerificationGate",
43
+ ]