loopy-loop 0.2.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.
- loopy_loop/__init__.py +1 -0
- loopy_loop/cli.py +293 -0
- loopy_loop/config.py +491 -0
- loopy_loop/coordinator_app.py +706 -0
- loopy_loop/harness_runner.py +195 -0
- loopy_loop/models.py +163 -0
- loopy_loop/scheduler.py +232 -0
- loopy_loop/sessions.py +313 -0
- loopy_loop/state_store.py +110 -0
- loopy_loop/templates/inner_outer_eval/.gitignore +4 -0
- loopy_loop/templates/inner_outer_eval/.loopy_loop/workflow_sets/inner_outer_eval/workflows/eval_reviewer/config.yaml +11 -0
- loopy_loop/templates/inner_outer_eval/.loopy_loop/workflow_sets/inner_outer_eval/workflows/eval_reviewer/prompt.txt +97 -0
- loopy_loop/templates/inner_outer_eval/.loopy_loop/workflow_sets/inner_outer_eval/workflows/eval_runner/config.yaml +12 -0
- loopy_loop/templates/inner_outer_eval/.loopy_loop/workflow_sets/inner_outer_eval/workflows/eval_runner/prompt.txt +86 -0
- loopy_loop/templates/inner_outer_eval/.loopy_loop/workflow_sets/inner_outer_eval/workflows/inner/config.yaml +8 -0
- loopy_loop/templates/inner_outer_eval/.loopy_loop/workflow_sets/inner_outer_eval/workflows/inner/prompt.txt +223 -0
- loopy_loop/templates/inner_outer_eval/.loopy_loop/workflow_sets/inner_outer_eval/workflows/outer/config.yaml +8 -0
- loopy_loop/templates/inner_outer_eval/.loopy_loop/workflow_sets/inner_outer_eval/workflows/outer/prompt.txt +333 -0
- loopy_loop/templates/inner_outer_eval/loopy_loop_config.yaml +45 -0
- loopy_loop/templates/inner_outer_eval/loopy_loop_goal.txt +5 -0
- loopy_loop/templates/pm_planner_dispatcher/.gitignore +1 -0
- loopy_loop/templates/pm_planner_dispatcher/.loopy_loop/workflow_sets/pm_planner_dispatcher/workflows/dispatcher/config.yaml +8 -0
- loopy_loop/templates/pm_planner_dispatcher/.loopy_loop/workflow_sets/pm_planner_dispatcher/workflows/dispatcher/prompt.txt +78 -0
- loopy_loop/templates/pm_planner_dispatcher/.loopy_loop/workflow_sets/pm_planner_dispatcher/workflows/planner/config.yaml +9 -0
- loopy_loop/templates/pm_planner_dispatcher/.loopy_loop/workflow_sets/pm_planner_dispatcher/workflows/planner/prompt.txt +99 -0
- loopy_loop/templates/pm_planner_dispatcher/loopy_loop_config.yaml +24 -0
- loopy_loop/templates/pm_planner_dispatcher/loopy_loop_goal.txt +1 -0
- loopy_loop/worker.py +265 -0
- loopy_loop-0.2.0.dist-info/METADATA +408 -0
- loopy_loop-0.2.0.dist-info/RECORD +33 -0
- loopy_loop-0.2.0.dist-info/WHEEL +4 -0
- loopy_loop-0.2.0.dist-info/entry_points.txt +3 -0
- loopy_loop-0.2.0.dist-info/licenses/LICENSE +201 -0
loopy_loop/sessions.py
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import uuid
|
|
6
|
+
|
|
7
|
+
from loopy_loop.config import LOOPY_DIRNAME
|
|
8
|
+
from loopy_loop.models import utc_now
|
|
9
|
+
|
|
10
|
+
SESSIONS_DIRNAME = "sessions"
|
|
11
|
+
ITERATIONS_DIRNAME = "iterations"
|
|
12
|
+
CHILDREN_DIRNAME = "children"
|
|
13
|
+
CHILD_REQUESTS_DIRNAME = "child_requests"
|
|
14
|
+
SESSION_METADATA_FILENAME = "session.json"
|
|
15
|
+
STATE_FILENAME = "state.json"
|
|
16
|
+
CHILDREN_FILENAME = "children.json"
|
|
17
|
+
PARENT_FILENAME = "parent.json"
|
|
18
|
+
GOAL_FILENAME = "goal.md"
|
|
19
|
+
EVENTS_FILENAME = "events.jsonl"
|
|
20
|
+
PROJECT_STATE_DIRNAME = "project_state"
|
|
21
|
+
EVAL_CHECKS_DIRNAME = "eval_checks"
|
|
22
|
+
HARNESS_OUTPUTS_DIRNAME = "harness_outputs"
|
|
23
|
+
UPDATES_FROM_USER_FILENAME = "updates_from_user.md"
|
|
24
|
+
FINISHED_FILENAME = "finished.md"
|
|
25
|
+
PROMPT_FILENAME = "prompt.txt"
|
|
26
|
+
RESULT_FILENAME = "result.json"
|
|
27
|
+
RESULT_TEXT_FILENAME = "result_text.txt"
|
|
28
|
+
HARNESS_RUN_ID_FILENAME = "harness_run_id.txt"
|
|
29
|
+
PENDING_FINISHED_REQUEST_FILENAME = "pending_finished_request.json"
|
|
30
|
+
CONTROL_FILENAME = "control.json"
|
|
31
|
+
GOAL_CHECK_FILENAME = "goal_check.json"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def create_session_id(*, goal_hash: str) -> str:
|
|
35
|
+
stamp = utc_now().strftime("%Y%m%d_%H%M%S")
|
|
36
|
+
unique = uuid.uuid4().hex[:8]
|
|
37
|
+
return f"{stamp}_{goal_hash}_{unique}"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def create_session_dir(
|
|
41
|
+
*,
|
|
42
|
+
repo_root: Path,
|
|
43
|
+
session_id: str,
|
|
44
|
+
goal_hash: str,
|
|
45
|
+
goal: str = "",
|
|
46
|
+
workflow_set: str,
|
|
47
|
+
parent_session_id: str | None = None,
|
|
48
|
+
) -> Path:
|
|
49
|
+
created_at = utc_now().isoformat().replace("+00:00", "Z")
|
|
50
|
+
if parent_session_id is None:
|
|
51
|
+
session_dir = sessions_root_path(repo_root=repo_root) / session_id
|
|
52
|
+
else:
|
|
53
|
+
session_dir = (
|
|
54
|
+
session_dir_path(repo_root=repo_root, session_id=parent_session_id)
|
|
55
|
+
/ CHILDREN_DIRNAME
|
|
56
|
+
/ session_id
|
|
57
|
+
)
|
|
58
|
+
session_dir.mkdir(parents=True, exist_ok=True)
|
|
59
|
+
metadata_path = session_dir / SESSION_METADATA_FILENAME
|
|
60
|
+
if not metadata_path.exists():
|
|
61
|
+
payload = {
|
|
62
|
+
"session_id": session_id,
|
|
63
|
+
"goal_hash": goal_hash,
|
|
64
|
+
"workflow_set": workflow_set,
|
|
65
|
+
"parent_session_id": parent_session_id,
|
|
66
|
+
"created_at": created_at,
|
|
67
|
+
}
|
|
68
|
+
metadata_path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
|
|
69
|
+
if parent_session_id is not None:
|
|
70
|
+
parent_path = session_dir / PARENT_FILENAME
|
|
71
|
+
if not parent_path.exists():
|
|
72
|
+
payload = {
|
|
73
|
+
"schema_version": 1,
|
|
74
|
+
"parent_session_id": parent_session_id,
|
|
75
|
+
"parent_relative_path": "../..",
|
|
76
|
+
"created_at": created_at,
|
|
77
|
+
}
|
|
78
|
+
parent_path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
|
|
79
|
+
goal_path = session_dir / GOAL_FILENAME
|
|
80
|
+
if goal and not goal_path.exists():
|
|
81
|
+
goal_path.write_text(goal.rstrip() + "\n", encoding="utf-8")
|
|
82
|
+
children = session_dir / CHILDREN_FILENAME
|
|
83
|
+
if not children.exists():
|
|
84
|
+
payload = {"schema_version": 1, "children": []}
|
|
85
|
+
children.write_text(json.dumps(payload, indent=2), encoding="utf-8")
|
|
86
|
+
child_requests_dir_path(repo_root=repo_root, session_id=session_id).mkdir(
|
|
87
|
+
parents=True, exist_ok=True
|
|
88
|
+
)
|
|
89
|
+
events_path = session_dir / EVENTS_FILENAME
|
|
90
|
+
if not events_path.exists():
|
|
91
|
+
events_path.write_text("", encoding="utf-8")
|
|
92
|
+
control_path(repo_root=repo_root, session_id=session_id)
|
|
93
|
+
updates_path = updates_from_user_path(repo_root=repo_root, session_id=session_id)
|
|
94
|
+
if not updates_path.exists():
|
|
95
|
+
updates_path.write_text("", encoding="utf-8")
|
|
96
|
+
project_state_dir_path(repo_root=repo_root, session_id=session_id).mkdir(
|
|
97
|
+
parents=True, exist_ok=True
|
|
98
|
+
)
|
|
99
|
+
finished = finished_path(repo_root=repo_root, session_id=session_id)
|
|
100
|
+
if not finished.exists():
|
|
101
|
+
finished.write_text("# Finished Work\n", encoding="utf-8")
|
|
102
|
+
eval_checks_dir_path(repo_root=repo_root, session_id=session_id).mkdir(
|
|
103
|
+
parents=True, exist_ok=True
|
|
104
|
+
)
|
|
105
|
+
harness_outputs_dir_path(repo_root=repo_root, session_id=session_id).mkdir(
|
|
106
|
+
parents=True, exist_ok=True
|
|
107
|
+
)
|
|
108
|
+
iterations_dir_path(repo_root=repo_root, session_id=session_id).mkdir(
|
|
109
|
+
parents=True, exist_ok=True
|
|
110
|
+
)
|
|
111
|
+
return session_dir
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def sessions_root_path(*, repo_root: Path) -> Path:
|
|
115
|
+
return repo_root / LOOPY_DIRNAME / SESSIONS_DIRNAME
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def session_dir_path(*, repo_root: Path, session_id: str) -> Path:
|
|
119
|
+
root = sessions_root_path(repo_root=repo_root)
|
|
120
|
+
direct = root / session_id
|
|
121
|
+
if direct.exists():
|
|
122
|
+
return direct
|
|
123
|
+
if root.exists():
|
|
124
|
+
for candidate in sorted(root.rglob(session_id)):
|
|
125
|
+
if candidate.is_dir() and candidate.name == session_id:
|
|
126
|
+
return candidate
|
|
127
|
+
return direct
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def state_path(*, repo_root: Path, session_id: str) -> Path:
|
|
131
|
+
return session_dir_path(repo_root=repo_root, session_id=session_id) / STATE_FILENAME
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def children_path(*, repo_root: Path, session_id: str) -> Path:
|
|
135
|
+
return (
|
|
136
|
+
session_dir_path(repo_root=repo_root, session_id=session_id) / CHILDREN_FILENAME
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def parent_path(*, repo_root: Path, session_id: str) -> Path:
|
|
141
|
+
return (
|
|
142
|
+
session_dir_path(repo_root=repo_root, session_id=session_id) / PARENT_FILENAME
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def child_requests_dir_path(*, repo_root: Path, session_id: str) -> Path:
|
|
147
|
+
return (
|
|
148
|
+
session_dir_path(repo_root=repo_root, session_id=session_id)
|
|
149
|
+
/ CHILD_REQUESTS_DIRNAME
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def child_sessions_dir_path(*, repo_root: Path, session_id: str) -> Path:
|
|
154
|
+
return (
|
|
155
|
+
session_dir_path(repo_root=repo_root, session_id=session_id) / CHILDREN_DIRNAME
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def session_goal_path(*, repo_root: Path, session_id: str) -> Path:
|
|
160
|
+
return session_dir_path(repo_root=repo_root, session_id=session_id) / GOAL_FILENAME
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def latest_top_level_state_path(*, repo_root: Path) -> Path | None:
|
|
164
|
+
root = sessions_root_path(repo_root=repo_root)
|
|
165
|
+
if not root.exists():
|
|
166
|
+
return None
|
|
167
|
+
candidates = [
|
|
168
|
+
path / STATE_FILENAME
|
|
169
|
+
for path in root.iterdir()
|
|
170
|
+
if path.is_dir() and (path / STATE_FILENAME).exists()
|
|
171
|
+
]
|
|
172
|
+
return sorted(candidates)[-1] if candidates else None
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def latest_state_path(*, repo_root: Path) -> Path | None:
|
|
176
|
+
root = sessions_root_path(repo_root=repo_root)
|
|
177
|
+
if not root.exists():
|
|
178
|
+
return None
|
|
179
|
+
candidates = sorted(root.rglob(STATE_FILENAME))
|
|
180
|
+
return candidates[-1] if candidates else None
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def iterations_dir_path(*, repo_root: Path, session_id: str) -> Path:
|
|
184
|
+
return (
|
|
185
|
+
session_dir_path(repo_root=repo_root, session_id=session_id)
|
|
186
|
+
/ ITERATIONS_DIRNAME
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def project_state_dir_path(*, repo_root: Path, session_id: str) -> Path:
|
|
191
|
+
return (
|
|
192
|
+
session_dir_path(repo_root=repo_root, session_id=session_id)
|
|
193
|
+
/ PROJECT_STATE_DIRNAME
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def eval_checks_dir_path(*, repo_root: Path, session_id: str) -> Path:
|
|
198
|
+
return (
|
|
199
|
+
session_dir_path(repo_root=repo_root, session_id=session_id)
|
|
200
|
+
/ EVAL_CHECKS_DIRNAME
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def harness_outputs_dir_path(*, repo_root: Path, session_id: str) -> Path:
|
|
205
|
+
return (
|
|
206
|
+
session_dir_path(repo_root=repo_root, session_id=session_id)
|
|
207
|
+
/ HARNESS_OUTPUTS_DIRNAME
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def updates_from_user_path(*, repo_root: Path, session_id: str) -> Path:
|
|
212
|
+
return (
|
|
213
|
+
session_dir_path(repo_root=repo_root, session_id=session_id)
|
|
214
|
+
/ UPDATES_FROM_USER_FILENAME
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def finished_path(*, repo_root: Path, session_id: str) -> Path:
|
|
219
|
+
return (
|
|
220
|
+
project_state_dir_path(repo_root=repo_root, session_id=session_id)
|
|
221
|
+
/ FINISHED_FILENAME
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def iteration_dir_name(*, iteration: int, workflow_id: str) -> str:
|
|
226
|
+
return f"{iteration:04d}_{workflow_id}"
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def iteration_dir_path(
|
|
230
|
+
*, repo_root: Path, session_id: str, iteration: int, workflow_id: str
|
|
231
|
+
) -> Path:
|
|
232
|
+
return iterations_dir_path(repo_root=repo_root, session_id=session_id) / (
|
|
233
|
+
iteration_dir_name(iteration=iteration, workflow_id=workflow_id)
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def iteration_harness_output_root(
|
|
238
|
+
*, repo_root: Path, session_id: str, iteration: int, workflow_id: str
|
|
239
|
+
) -> Path:
|
|
240
|
+
return harness_outputs_dir_path(
|
|
241
|
+
repo_root=repo_root, session_id=session_id
|
|
242
|
+
) / iteration_dir_name(iteration=iteration, workflow_id=workflow_id)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def ensure_iteration_dir(
|
|
246
|
+
*, repo_root: Path, session_id: str, iteration: int, workflow_id: str
|
|
247
|
+
) -> Path:
|
|
248
|
+
iteration_dir = iteration_dir_path(
|
|
249
|
+
repo_root=repo_root,
|
|
250
|
+
session_id=session_id,
|
|
251
|
+
iteration=iteration,
|
|
252
|
+
workflow_id=workflow_id,
|
|
253
|
+
)
|
|
254
|
+
iteration_dir.mkdir(parents=True, exist_ok=True)
|
|
255
|
+
return iteration_dir
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def pending_finished_request_path(
|
|
259
|
+
*, repo_root: Path, session_id: str, iteration: int, workflow_id: str
|
|
260
|
+
) -> Path:
|
|
261
|
+
return (
|
|
262
|
+
iteration_dir_path(
|
|
263
|
+
repo_root=repo_root,
|
|
264
|
+
session_id=session_id,
|
|
265
|
+
iteration=iteration,
|
|
266
|
+
workflow_id=workflow_id,
|
|
267
|
+
)
|
|
268
|
+
/ PENDING_FINISHED_REQUEST_FILENAME
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def result_path(
|
|
273
|
+
*, repo_root: Path, session_id: str, iteration: int, workflow_id: str
|
|
274
|
+
) -> Path:
|
|
275
|
+
return (
|
|
276
|
+
iteration_dir_path(
|
|
277
|
+
repo_root=repo_root,
|
|
278
|
+
session_id=session_id,
|
|
279
|
+
iteration=iteration,
|
|
280
|
+
workflow_id=workflow_id,
|
|
281
|
+
)
|
|
282
|
+
/ RESULT_FILENAME
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def control_path(*, repo_root: Path, session_id: str) -> Path:
|
|
287
|
+
path = (
|
|
288
|
+
session_dir_path(repo_root=repo_root, session_id=session_id) / CONTROL_FILENAME
|
|
289
|
+
)
|
|
290
|
+
if not path.exists():
|
|
291
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
292
|
+
payload = {
|
|
293
|
+
"state": "running",
|
|
294
|
+
"reason": "session active",
|
|
295
|
+
"stop_reason": None,
|
|
296
|
+
"schema_version": 1,
|
|
297
|
+
}
|
|
298
|
+
path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
|
|
299
|
+
return path
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def goal_check_path(
|
|
303
|
+
*, repo_root: Path, session_id: str, iteration: int, workflow_id: str = "goal_check"
|
|
304
|
+
) -> Path:
|
|
305
|
+
return (
|
|
306
|
+
ensure_iteration_dir(
|
|
307
|
+
repo_root=repo_root,
|
|
308
|
+
session_id=session_id,
|
|
309
|
+
iteration=iteration,
|
|
310
|
+
workflow_id=workflow_id,
|
|
311
|
+
)
|
|
312
|
+
/ GOAL_CHECK_FILENAME
|
|
313
|
+
)
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import TypeVar
|
|
7
|
+
|
|
8
|
+
from filelock import FileLock
|
|
9
|
+
|
|
10
|
+
from loopy_loop.config import LOOPY_DIRNAME
|
|
11
|
+
from loopy_loop.models import DEFAULT_LOCK_TIMEOUT_SECONDS
|
|
12
|
+
from loopy_loop.models import LoopState
|
|
13
|
+
from loopy_loop.models import utc_now
|
|
14
|
+
from loopy_loop.sessions import latest_top_level_state_path
|
|
15
|
+
from loopy_loop.sessions import state_path as session_state_path
|
|
16
|
+
|
|
17
|
+
STATE_FILENAME = "state.json"
|
|
18
|
+
LOCK_FILENAME = "state.json.lock"
|
|
19
|
+
TEMP_FILENAME = "state.json.tmp"
|
|
20
|
+
ARCHIVE_PREFIX = "state.json.archive_"
|
|
21
|
+
TERMINAL_STATUSES = {"stopped", "goal_met", "failed", "max_turns"}
|
|
22
|
+
|
|
23
|
+
T = TypeVar("T")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class StateStore:
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
*,
|
|
30
|
+
repo_root: Path,
|
|
31
|
+
state_path: Path | None = None,
|
|
32
|
+
lock_timeout_seconds: float = DEFAULT_LOCK_TIMEOUT_SECONDS,
|
|
33
|
+
) -> None:
|
|
34
|
+
self.repo_root = repo_root
|
|
35
|
+
self.loopy_dir = repo_root / LOOPY_DIRNAME
|
|
36
|
+
self.state_path = state_path
|
|
37
|
+
self.lock_timeout_seconds = lock_timeout_seconds
|
|
38
|
+
|
|
39
|
+
def read_state(self) -> LoopState | None:
|
|
40
|
+
with self._lock():
|
|
41
|
+
return self._read_state_unlocked()
|
|
42
|
+
|
|
43
|
+
def write_state(self, *, state: LoopState) -> LoopState:
|
|
44
|
+
with self._lock():
|
|
45
|
+
self._write_state_unlocked(state=state)
|
|
46
|
+
return state
|
|
47
|
+
|
|
48
|
+
def mutate(self, mutator: Callable[[LoopState | None], tuple[LoopState, T]]) -> T:
|
|
49
|
+
with self._lock():
|
|
50
|
+
current = self._read_state_unlocked()
|
|
51
|
+
next_state, result = mutator(current)
|
|
52
|
+
self._write_state_unlocked(state=next_state)
|
|
53
|
+
return result
|
|
54
|
+
|
|
55
|
+
def archive_state(self) -> Path | None:
|
|
56
|
+
with self._lock():
|
|
57
|
+
current = self._read_state_unlocked()
|
|
58
|
+
if current is None or self.state_path is None:
|
|
59
|
+
return None
|
|
60
|
+
archive_path = self._archive_path()
|
|
61
|
+
os.replace(self.state_path, archive_path)
|
|
62
|
+
return archive_path
|
|
63
|
+
|
|
64
|
+
def clear_state(self) -> None:
|
|
65
|
+
with self._lock():
|
|
66
|
+
if self.state_path is not None and self.state_path.exists():
|
|
67
|
+
self.state_path.unlink()
|
|
68
|
+
|
|
69
|
+
def is_terminal_state(self, *, state: LoopState) -> bool:
|
|
70
|
+
return state.status in TERMINAL_STATUSES
|
|
71
|
+
|
|
72
|
+
def _lock(self) -> FileLock:
|
|
73
|
+
self.loopy_dir.mkdir(parents=True, exist_ok=True)
|
|
74
|
+
lock_path = self._effective_state_path()
|
|
75
|
+
if lock_path is None:
|
|
76
|
+
lock_path = self.loopy_dir / LOCK_FILENAME
|
|
77
|
+
else:
|
|
78
|
+
lock_path = lock_path.with_name(LOCK_FILENAME)
|
|
79
|
+
lock_path.parent.mkdir(parents=True, exist_ok=True)
|
|
80
|
+
return FileLock(str(lock_path), timeout=self.lock_timeout_seconds)
|
|
81
|
+
|
|
82
|
+
def _read_state_unlocked(self) -> LoopState | None:
|
|
83
|
+
state_path = self._effective_state_path()
|
|
84
|
+
if state_path is None or not state_path.exists():
|
|
85
|
+
return None
|
|
86
|
+
self.state_path = state_path
|
|
87
|
+
raw = state_path.read_text(encoding="utf-8")
|
|
88
|
+
return LoopState.model_validate_json(raw)
|
|
89
|
+
|
|
90
|
+
def _write_state_unlocked(self, *, state: LoopState) -> None:
|
|
91
|
+
if self.state_path is None:
|
|
92
|
+
self.state_path = session_state_path(
|
|
93
|
+
repo_root=self.repo_root, session_id=state.active_session_id
|
|
94
|
+
)
|
|
95
|
+
self.state_path.parent.mkdir(parents=True, exist_ok=True)
|
|
96
|
+
temp_path = self.state_path.with_name(TEMP_FILENAME)
|
|
97
|
+
payload = state.model_dump_json(indent=2)
|
|
98
|
+
temp_path.write_text(payload, encoding="utf-8")
|
|
99
|
+
os.replace(temp_path, self.state_path)
|
|
100
|
+
|
|
101
|
+
def _archive_path(self) -> Path:
|
|
102
|
+
if self.state_path is None:
|
|
103
|
+
raise RuntimeError("Cannot archive without a state path")
|
|
104
|
+
stamp = utc_now().strftime("%Y%m%dT%H%M%SZ")
|
|
105
|
+
return self.state_path.with_name(f"{ARCHIVE_PREFIX}{stamp}.json")
|
|
106
|
+
|
|
107
|
+
def _effective_state_path(self) -> Path | None:
|
|
108
|
+
if self.state_path is not None:
|
|
109
|
+
return self.state_path
|
|
110
|
+
return latest_top_level_state_path(repo_root=self.repo_root)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
enabled: true
|
|
2
|
+
priority: 100
|
|
3
|
+
run_every: 1
|
|
4
|
+
must_follow: null
|
|
5
|
+
not_before_iteration: 0
|
|
6
|
+
run_on_start: true
|
|
7
|
+
run_after_successes:
|
|
8
|
+
workflow_id: inner
|
|
9
|
+
every: 10
|
|
10
|
+
description: |
|
|
11
|
+
Create or refresh high-level eval-banana checks for the active session.
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
You are the eval reviewer for this loopy-loop session.
|
|
2
|
+
|
|
3
|
+
Your job is to create and maintain high-level eval-banana scenarios that can
|
|
4
|
+
decide whether the configured goal and completion criteria have been met.
|
|
5
|
+
Keep the scenarios general enough to allow different implementation choices.
|
|
6
|
+
|
|
7
|
+
Inputs available in the rendered assignment:
|
|
8
|
+
- Goal, completion criteria, and stop criteria
|
|
9
|
+
- Session directory
|
|
10
|
+
- Session project_state directory
|
|
11
|
+
- Session eval_checks directory
|
|
12
|
+
- Session finished ledger path
|
|
13
|
+
- Session harness outputs directory
|
|
14
|
+
- Iteration directory
|
|
15
|
+
- Iteration harness output root
|
|
16
|
+
|
|
17
|
+
Goal source of truth:
|
|
18
|
+
- Treat the rendered Goal input and loopy_loop_goal.txt as canonical.
|
|
19
|
+
- Do not infer or restate the goal from project_state files.
|
|
20
|
+
- Use project_state only to understand progress, decisions, memory, and eval
|
|
21
|
+
history.
|
|
22
|
+
|
|
23
|
+
State contract:
|
|
24
|
+
- Create the session project_state directory if it does not exist.
|
|
25
|
+
- Create the session eval_checks directory if it does not exist.
|
|
26
|
+
- Ensure project_state/README.md explains:
|
|
27
|
+
- loopy_loop_goal.txt is the goal source of truth
|
|
28
|
+
- memory.md is essential durable facts only
|
|
29
|
+
- current_state.md is live status and latest eval headline only
|
|
30
|
+
- eval_results.md owns eval check inventory, run history, and report links
|
|
31
|
+
- finished.md is outer-owned accepted completions only
|
|
32
|
+
- Ensure project_state/memory.md exists and stays limited to essential durable
|
|
33
|
+
facts future iterations must keep in working memory. It is not iteration
|
|
34
|
+
history and not decision rationale.
|
|
35
|
+
- Read and update project_state/current_state.md and project_state/eval_results.md.
|
|
36
|
+
- Read project_state/memory.md when creating or refreshing checks.
|
|
37
|
+
- Do not mark implementation tasks complete. That belongs to outer after review.
|
|
38
|
+
|
|
39
|
+
Eval state split:
|
|
40
|
+
- Keep eval check inventory, commands, validation result, run result, report
|
|
41
|
+
paths, and missing/weak checks in project_state/eval_results.md.
|
|
42
|
+
- Keep project_state/current_state.md limited to eval readiness headline and
|
|
43
|
+
next action.
|
|
44
|
+
- Do not copy full eval reports into current_state.md.
|
|
45
|
+
|
|
46
|
+
PR and merge guardrail:
|
|
47
|
+
- Do not create branches, open PRs, or merge PRs. Eval workflows only create or
|
|
48
|
+
refresh eval checks and eval state.
|
|
49
|
+
|
|
50
|
+
Eval-banana contract:
|
|
51
|
+
- Check files are YAML files.
|
|
52
|
+
- Use schema_version: 1.
|
|
53
|
+
- Use unique check ids.
|
|
54
|
+
- Only create harness_judge checks. Use them to describe the desired outcomes.
|
|
55
|
+
These can be kept high level.
|
|
56
|
+
- Do not create deterministic checks. Deterministic checks are forbidden for
|
|
57
|
+
this workflow, even for objective file, structure, command, or data
|
|
58
|
+
assertions.
|
|
59
|
+
- target_paths are resolved from the repo root.
|
|
60
|
+
- Session-local checks should evaluate the repo by running eval-banana from the
|
|
61
|
+
repo root with --cwd . and --check-dir <session eval_checks directory>.
|
|
62
|
+
- The eval runner will run only this session's checks with:
|
|
63
|
+
eval-banana run --cwd . --check-dir <session eval_checks directory> --output-dir <session directory>/eval_results
|
|
64
|
+
|
|
65
|
+
What to do:
|
|
66
|
+
1. Read .agents/skills/eval-banana/SKILL.md if present. If it is absent, use
|
|
67
|
+
the eval-banana contract above.
|
|
68
|
+
2. Read the session project_state files if they exist, including memory.md.
|
|
69
|
+
3. Inspect the repo enough to understand the current implementation shape.
|
|
70
|
+
4. Create or refresh YAML harness_judge checks under the session eval_checks
|
|
71
|
+
directory.
|
|
72
|
+
5. Make the harness_judge checks high-level and outcome-focused. Avoid brittle
|
|
73
|
+
assertions about implementation details unless the completion criteria
|
|
74
|
+
require them.
|
|
75
|
+
6. Run eval-banana validate with --check-dir for the session eval_checks
|
|
76
|
+
directory when eval-banana is available.
|
|
77
|
+
7. Update project_state/eval_results.md with:
|
|
78
|
+
- active checks
|
|
79
|
+
- which goal condition each check covers
|
|
80
|
+
- last validation result
|
|
81
|
+
- last run result, if one exists
|
|
82
|
+
- known weak or missing checks
|
|
83
|
+
8. Update project_state/current_state.md with the current eval readiness state.
|
|
84
|
+
9. Update project_state/memory.md only if eval work discovers an essential
|
|
85
|
+
durable fact future iterations must remember.
|
|
86
|
+
10. Write any supporting reports under the iteration harness output root when useful.
|
|
87
|
+
|
|
88
|
+
Good check coverage should answer:
|
|
89
|
+
- Does the repo contain a runnable implementation for the goal?
|
|
90
|
+
- Are the configured completion criteria observably satisfied?
|
|
91
|
+
- Are there clear local run/test instructions where relevant?
|
|
92
|
+
- Are important user-facing workflows covered at a high level?
|
|
93
|
+
|
|
94
|
+
If eval-banana is unavailable, still write the YAML checks and record that
|
|
95
|
+
validation could not be run.
|
|
96
|
+
|
|
97
|
+
Use Codex.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
enabled: true
|
|
2
|
+
priority: 90
|
|
3
|
+
run_every: 1
|
|
4
|
+
must_follow: eval_reviewer
|
|
5
|
+
not_before_iteration: 10
|
|
6
|
+
run_after_successes:
|
|
7
|
+
workflow_id: inner
|
|
8
|
+
every: 10
|
|
9
|
+
emits_goal_check: true
|
|
10
|
+
description: |
|
|
11
|
+
Run the session eval-banana checks and write goal_check.json. The loop stops
|
|
12
|
+
only when all checks pass.
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
You are the eval runner for this loopy-loop session.
|
|
2
|
+
|
|
3
|
+
Your job is to run the session eval-banana checks and write the goal_check.json
|
|
4
|
+
file requested in the rendered assignment.
|
|
5
|
+
|
|
6
|
+
Inputs available in the rendered assignment:
|
|
7
|
+
- Goal, completion criteria, and stop criteria
|
|
8
|
+
- Session directory
|
|
9
|
+
- Session project_state directory
|
|
10
|
+
- Session eval_checks directory
|
|
11
|
+
- Session finished ledger path
|
|
12
|
+
- Session harness outputs directory
|
|
13
|
+
- Iteration directory
|
|
14
|
+
- Iteration harness output root
|
|
15
|
+
- Session control path
|
|
16
|
+
- goal_check.json output path
|
|
17
|
+
|
|
18
|
+
Goal source of truth:
|
|
19
|
+
- Treat the rendered Goal input and loopy_loop_goal.txt as canonical.
|
|
20
|
+
- Do not infer or restate the goal from project_state files.
|
|
21
|
+
- Use project_state only to understand progress, memory, and eval history.
|
|
22
|
+
|
|
23
|
+
Eval state split:
|
|
24
|
+
- project_state/eval_results.md owns eval commands, validation status, run ids,
|
|
25
|
+
report paths, score, pass/fail history, and failed-check details.
|
|
26
|
+
- project_state/current_state.md should contain only the latest eval headline
|
|
27
|
+
and the next action.
|
|
28
|
+
- Do not copy full eval reports into current_state.md.
|
|
29
|
+
|
|
30
|
+
PR and merge guardrail:
|
|
31
|
+
- Do not create branches, open PRs, or merge PRs. Eval workflows only run evals,
|
|
32
|
+
update eval state, and write goal_check.json.
|
|
33
|
+
|
|
34
|
+
What to do:
|
|
35
|
+
1. Read project_state/memory.md, project_state/current_state.md, and
|
|
36
|
+
project_state/eval_results.md if they exist.
|
|
37
|
+
2. Confirm the session eval_checks directory exists and contains YAML checks.
|
|
38
|
+
3. If eval-banana is available, create <session directory>/eval_results and run
|
|
39
|
+
from the repo root. Pass --cwd . so session-local checks evaluate repo-root
|
|
40
|
+
paths:
|
|
41
|
+
eval-banana validate --cwd . --check-dir <session eval_checks directory>
|
|
42
|
+
eval-banana run --cwd . --check-dir <session eval_checks directory> --output-dir <session directory>/eval_results
|
|
43
|
+
4. Read the eval-banana console output and the latest report.json/report.md
|
|
44
|
+
under <session directory>/eval_results/<run_id>/ when available.
|
|
45
|
+
5. Update project_state/eval_results.md as a curated eval index with:
|
|
46
|
+
- iteration number
|
|
47
|
+
- commands run
|
|
48
|
+
- validation status
|
|
49
|
+
- run id and report path when available
|
|
50
|
+
- score and pass/fail status
|
|
51
|
+
- failed checks with concise reasons
|
|
52
|
+
- next eval-facing action
|
|
53
|
+
6. Update project_state/current_state.md with a short latest-eval summary:
|
|
54
|
+
goal_met, pass/fail, run id/report path, and what should happen next.
|
|
55
|
+
7. Update project_state/memory.md only if the eval result changes an essential
|
|
56
|
+
durable fact future iterations must remember.
|
|
57
|
+
8. Do not update project_state/finished.md. Do not mark implementation work
|
|
58
|
+
complete.
|
|
59
|
+
9. Write supporting eval logs or reports under the iteration harness output root
|
|
60
|
+
when useful.
|
|
61
|
+
10. Write goal_check.json to the exact path from the rendered assignment.
|
|
62
|
+
11. If and only if goal_check.json has goal_met true, update the rendered
|
|
63
|
+
Session control path to stop the loop with stop_reason goal_met.
|
|
64
|
+
|
|
65
|
+
goal_check.json schema:
|
|
66
|
+
{
|
|
67
|
+
"goal_met": false,
|
|
68
|
+
"reason": "brief explanation",
|
|
69
|
+
"schema_version": 1
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
Set goal_met to true only when:
|
|
73
|
+
- eval-banana validation succeeds,
|
|
74
|
+
- eval-banana run succeeds,
|
|
75
|
+
- all scenarios pass.
|
|
76
|
+
|
|
77
|
+
If eval-banana is unavailable, checks are missing, validation fails, or any
|
|
78
|
+
scenario fails, set goal_met to false and explain why in the reason.
|
|
79
|
+
|
|
80
|
+
Session control stop schema:
|
|
81
|
+
{
|
|
82
|
+
"state": "stopped",
|
|
83
|
+
"reason": "eval-banana validation and run passed; all scenarios passed",
|
|
84
|
+
"stop_reason": "goal_met",
|
|
85
|
+
"schema_version": 1
|
|
86
|
+
}
|