opal-optimizer 0.3.5__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.
- opal/__init__.py +7 -0
- opal/_version.py +3 -0
- opal/agent.py +394 -0
- opal/backends/__init__.py +31 -0
- opal/backends/_codex_client.py +450 -0
- opal/backends/_privacy.py +116 -0
- opal/backends/base.py +248 -0
- opal/backends/claude.py +665 -0
- opal/backends/codex.py +356 -0
- opal/backends/mailbox.py +1034 -0
- opal/backends/unavailable.py +161 -0
- opal/cli.py +396 -0
- opal/context.py +111 -0
- opal/defaults.json +11 -0
- opal/defaults.py +54 -0
- opal/estimate.py +120 -0
- opal/hf.py +109 -0
- opal/misc/__init__.py +2 -0
- opal/misc/aggressive.py +157 -0
- opal/misc/audit.py +84 -0
- opal/misc/boundary.py +226 -0
- opal/misc/checkpoint.py +399 -0
- opal/misc/ddp.py +147 -0
- opal/misc/integrity.py +263 -0
- opal/misc/nested.py +67 -0
- opal/misc/neutral_analysis.py +166 -0
- opal/misc/precision.py +210 -0
- opal/misc/recovery.py +231 -0
- opal/misc/schedulers.py +214 -0
- opal/misc/setup.py +253 -0
- opal/misc/skip.py +165 -0
- opal/misc/taskfacts.py +293 -0
- opal/misc/waiting.py +58 -0
- opal/programs.py +482 -0
- opal/prompts/example_updater.py +252 -0
- opal/prompts/neutral.md +206 -0
- opal/report.py +1001 -0
- opal/report_page.py +175 -0
- opal/report_privacy.py +159 -0
- opal/results.py +80 -0
- opal/schedule.py +134 -0
- opal/summaries.py +224 -0
- opal/wrapper.py +1674 -0
- opal_optimizer-0.3.5.dist-info/METADATA +232 -0
- opal_optimizer-0.3.5.dist-info/RECORD +49 -0
- opal_optimizer-0.3.5.dist-info/WHEEL +5 -0
- opal_optimizer-0.3.5.dist-info/entry_points.txt +2 -0
- opal_optimizer-0.3.5.dist-info/licenses/LICENSE +21 -0
- opal_optimizer-0.3.5.dist-info/top_level.txt +1 -0
opal/__init__.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""OPAL (OPtimizer with an Agent in the Loop): a coding agent inside your training run, rewriting the optimizer while
|
|
2
|
+
it trains."""
|
|
3
|
+
|
|
4
|
+
from ._version import __version__
|
|
5
|
+
from .wrapper import AgentOptimizer, final_program, replay, wrap
|
|
6
|
+
|
|
7
|
+
__all__ = ["wrap", "replay", "final_program", "AgentOptimizer"]
|
opal/_version.py
ADDED
opal/agent.py
ADDED
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
"""The agent side of a run on rank 0: run folder, one decision at a time in a background thread, the message text."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import datetime
|
|
6
|
+
import json
|
|
7
|
+
import math
|
|
8
|
+
import os
|
|
9
|
+
import secrets
|
|
10
|
+
import sys
|
|
11
|
+
import threading
|
|
12
|
+
import time
|
|
13
|
+
import traceback
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from . import programs
|
|
17
|
+
from .misc import aggressive, audit
|
|
18
|
+
from .backends._privacy import redact_secrets
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Run:
|
|
22
|
+
"""Everything a run leaves behind: workspace (what the agent sees), programs/vNNN (every applied version),
|
|
23
|
+
decisions/NNN (message, backend transcript, response), events.jsonl, reports.jsonl, losses.jsonl."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, path):
|
|
26
|
+
self.dir = Path(path).resolve()
|
|
27
|
+
self.workspace = self.dir / "workspace"
|
|
28
|
+
self.programs = self.dir / "programs"
|
|
29
|
+
self.decisions = self.dir / "decisions"
|
|
30
|
+
for d in (self.workspace / "inbox", self.workspace / "reference", self.programs, self.decisions):
|
|
31
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
32
|
+
self._notes_seen = 0
|
|
33
|
+
|
|
34
|
+
def event(self, kind, **kw):
|
|
35
|
+
rec = dict(kind=kind, utc=datetime.datetime.now(datetime.timezone.utc).isoformat(timespec="seconds"), **kw)
|
|
36
|
+
self._write("events.jsonl", rec)
|
|
37
|
+
|
|
38
|
+
def append(self, name, rec):
|
|
39
|
+
self._write(name, rec)
|
|
40
|
+
|
|
41
|
+
def _write(self, name, rec):
|
|
42
|
+
"""Records never stop training: on a full disk or an exhausted quota, warn once and go on."""
|
|
43
|
+
try:
|
|
44
|
+
with open(self.dir / name, "a") as f:
|
|
45
|
+
f.write(json.dumps(rec, default=str) + "\n")
|
|
46
|
+
except OSError as exc:
|
|
47
|
+
if not getattr(self, "_write_failed", False):
|
|
48
|
+
self._write_failed = True
|
|
49
|
+
print(
|
|
50
|
+
f"[opal] could not write the run records ({exc}); training continues",
|
|
51
|
+
file=sys.stderr,
|
|
52
|
+
flush=True,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
def new_notes(self):
|
|
56
|
+
f = self.dir / "notes.txt"
|
|
57
|
+
if not f.exists():
|
|
58
|
+
return ""
|
|
59
|
+
text = f.read_text()
|
|
60
|
+
new, self._notes_seen = text[self._notes_seen :], len(text)
|
|
61
|
+
return new.strip()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class AgentWorker:
|
|
65
|
+
def __init__(self, run: Run, backend, system_text: str, log=print):
|
|
66
|
+
self.run, self.backend, self.system, self.log = run, backend, system_text, log
|
|
67
|
+
self.thread = None
|
|
68
|
+
self.ready = None
|
|
69
|
+
self.k = 0
|
|
70
|
+
self.cost = 0.0
|
|
71
|
+
self.unknown_cost_decisions = set() # a request stays unknown until it reports a numeric charge
|
|
72
|
+
self.usage = None # present only for token-reporting backends (Codex, including through a mailbox)
|
|
73
|
+
self._last = None # (k, req) of the newest request
|
|
74
|
+
self.after_decision = None # fn(k, req, resp), called once a response is recorded (a late one: in training)
|
|
75
|
+
backend.setup(run.dir, run.workspace)
|
|
76
|
+
(run.dir / "system.md").write_text(system_text)
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def recorded_cost(self):
|
|
80
|
+
if self.unknown_cost_decisions or self.usage is not None or self.backend.name == "codex":
|
|
81
|
+
return None
|
|
82
|
+
return round(self.cost, 4)
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def busy(self) -> bool:
|
|
86
|
+
return self.ready is not None or (self.thread is not None and self.thread.is_alive())
|
|
87
|
+
|
|
88
|
+
def submit(self, req: dict) -> int:
|
|
89
|
+
assert not self.busy
|
|
90
|
+
self.k += 1
|
|
91
|
+
self.unknown_cost_decisions.add(self.k)
|
|
92
|
+
self._last = (self.k, req)
|
|
93
|
+
self.thread = threading.Thread(
|
|
94
|
+
target=self._decide, args=(self.k, req), name=f"opal-decision-{self.k}", daemon=True
|
|
95
|
+
)
|
|
96
|
+
self.thread.start()
|
|
97
|
+
return self.k
|
|
98
|
+
|
|
99
|
+
def wait(self):
|
|
100
|
+
if self.thread is not None:
|
|
101
|
+
self.thread.join()
|
|
102
|
+
|
|
103
|
+
def take(self):
|
|
104
|
+
resp, self.ready = self.ready, None
|
|
105
|
+
return resp
|
|
106
|
+
|
|
107
|
+
def collect_late(self):
|
|
108
|
+
"""An answer that reached the backend after it stopped waiting (a mailbox whose broker came back) while no
|
|
109
|
+
newer request is out: checked and recorded like any answer, so it is applied at the next update boundary."""
|
|
110
|
+
late = getattr(self.backend, "late_answer", None)
|
|
111
|
+
if late is None or self.busy or self._last is None:
|
|
112
|
+
return
|
|
113
|
+
try:
|
|
114
|
+
out = late()
|
|
115
|
+
except Exception:
|
|
116
|
+
return # an unreadable mailbox never stops training; the next request says the answer was discarded
|
|
117
|
+
if out is not None:
|
|
118
|
+
self._finish(*self._last, dict(out, late=True))
|
|
119
|
+
|
|
120
|
+
def _decide(self, k, req):
|
|
121
|
+
rec = self.run.decisions / f"{k:03d}"
|
|
122
|
+
ws = self.run.workspace
|
|
123
|
+
try:
|
|
124
|
+
rec.mkdir(parents=True, exist_ok=True)
|
|
125
|
+
(ws / "scratch").mkdir(exist_ok=True)
|
|
126
|
+
programs.write_dir(ws, req["files"]) # the workspace starts from the running programs at every request
|
|
127
|
+
(ws / "inbox" / f"decision-{k:03d}.json").write_text(json.dumps(req["inbox"], indent=1, default=str))
|
|
128
|
+
message = format_message(k, req)
|
|
129
|
+
(rec / "message.txt").write_text(message)
|
|
130
|
+
self.backend.log = lambda text, k=k: self.log(f"decision {k}: {redact_secrets(text)}")
|
|
131
|
+
self.backend.training_waits = bool(req.get("blocking")) # for the line when the agent is unavailable
|
|
132
|
+
out = self.backend.decide(ws, message, self.system, rec)
|
|
133
|
+
except Exception:
|
|
134
|
+
out = dict(text="", cost_usd=None, returncode=-1, error=traceback.format_exc(limit=6)[-3000:], seconds=0.0)
|
|
135
|
+
self._finish(k, req, out)
|
|
136
|
+
|
|
137
|
+
def _finish(self, k, req, out):
|
|
138
|
+
"""Check the programs the agent left in the workspace and record its answer as ready."""
|
|
139
|
+
rec, ws = self.run.decisions / f"{k:03d}", self.run.workspace
|
|
140
|
+
out = dict(out)
|
|
141
|
+
for field in ("error", "text"):
|
|
142
|
+
if out.get(field) is not None:
|
|
143
|
+
out[field] = redact_secrets(out[field])
|
|
144
|
+
files, changed, errors = None, False, []
|
|
145
|
+
try:
|
|
146
|
+
files = programs.program(programs.read_dir(ws))
|
|
147
|
+
changed = files != req["files"]
|
|
148
|
+
failed = out.get("error") or out.get("returncode") not in (0, None)
|
|
149
|
+
if changed and failed:
|
|
150
|
+
errors = [
|
|
151
|
+
"the agent call did not finish ("
|
|
152
|
+
+ str(out.get("error") or f"return code {out.get('returncode')}")[:300]
|
|
153
|
+
+ "); its edits were not applied"
|
|
154
|
+
]
|
|
155
|
+
elif changed:
|
|
156
|
+
errors = programs.check_syntax(files)
|
|
157
|
+
except Exception:
|
|
158
|
+
errors = ["could not read the edited files: " + traceback.format_exc(limit=3)[-800:]]
|
|
159
|
+
charge = out.get("cost_usd")
|
|
160
|
+
if isinstance(charge, (int, float)) and not isinstance(charge, bool) and math.isfinite(charge):
|
|
161
|
+
self.cost += float(charge)
|
|
162
|
+
self.unknown_cost_decisions.discard(k)
|
|
163
|
+
if isinstance(out.get("usage"), dict):
|
|
164
|
+
if self.usage is None:
|
|
165
|
+
self.usage = {}
|
|
166
|
+
for key, value in out["usage"].items():
|
|
167
|
+
self.usage[key] = self.usage.get(key, 0) + value
|
|
168
|
+
error = out.get("error")
|
|
169
|
+
if not error and out.get("returncode") not in (0, None): # a failed call always carries an error
|
|
170
|
+
error = f"the agent call ended with exit code {out.get('returncode')}"
|
|
171
|
+
resp = dict(
|
|
172
|
+
k=k,
|
|
173
|
+
requested_at=req["update"],
|
|
174
|
+
base=req["files"],
|
|
175
|
+
files=files if changed and not errors else None,
|
|
176
|
+
errors=errors,
|
|
177
|
+
text=(out.get("text") or "")[-8000:],
|
|
178
|
+
cost_usd=out.get("cost_usd"),
|
|
179
|
+
returncode=out.get("returncode"),
|
|
180
|
+
backend_error=error or None,
|
|
181
|
+
seconds=round(out.get("seconds") or 0.0, 1),
|
|
182
|
+
**({"unavailable_seconds": out["unavailable_seconds"]} if out.get("unavailable_seconds") else {}),
|
|
183
|
+
**{x: out[x] for x in ("usage", "session", "rollout") if x in out},
|
|
184
|
+
**{x: True for x in ("gave_up", "late") if out.get(x)},
|
|
185
|
+
)
|
|
186
|
+
try: # written whole, so that `opal show` never reads half a file
|
|
187
|
+
tmp = rec / "response.json.tmp"
|
|
188
|
+
tmp.write_text(json.dumps({x: v for x, v in resp.items() if x != "base"}, indent=1))
|
|
189
|
+
os.replace(tmp, rec / "response.json")
|
|
190
|
+
if errors and changed and files: # the workspace goes back to the running programs next time; keep them
|
|
191
|
+
programs.write_dir(rec / "rejected", files)
|
|
192
|
+
except Exception:
|
|
193
|
+
pass
|
|
194
|
+
if self.after_decision is not None:
|
|
195
|
+
try:
|
|
196
|
+
self.after_decision(k, req, resp)
|
|
197
|
+
except Exception:
|
|
198
|
+
pass
|
|
199
|
+
self.ready = resp
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _ms(v):
|
|
203
|
+
return "not measured yet" if v is None else f"{v:.3g} ms"
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _chunks(values, n=100):
|
|
207
|
+
if len(values) <= n:
|
|
208
|
+
return values, 1
|
|
209
|
+
size = -(-len(values) // n)
|
|
210
|
+
return [sum(values[i : i + size]) / len(values[i : i + size]) for i in range(0, len(values), size)], size
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def previous_answer(prev, last=None):
|
|
214
|
+
"""One sentence on what became of the agent's previous answer: when its edit took effect, or why it did not."""
|
|
215
|
+
if not prev:
|
|
216
|
+
return None
|
|
217
|
+
k, r, u, kind = prev["k"], prev.get("requested_at") or 0, prev.get("applied_at") or 0, prev.get("outcome")
|
|
218
|
+
if kind == "applied":
|
|
219
|
+
s = (
|
|
220
|
+
f"Your edit from decision {k} (requested at update {r:,}) took effect at update {u:,}, {u - r:,} updates "
|
|
221
|
+
"after the state you saw"
|
|
222
|
+
)
|
|
223
|
+
if prev.get("late"):
|
|
224
|
+
s += "; it arrived after the run had stopped waiting for it, and was applied when it came"
|
|
225
|
+
if last and last.get("k") == k and last.get("reverted_at"):
|
|
226
|
+
s += f"; it failed and was reverted at update {last['reverted_at']:,} (the errors below say why)"
|
|
227
|
+
return s + "."
|
|
228
|
+
if prev.get("gave_up"):
|
|
229
|
+
why = str(prev.get("backend_error") or "no answer").split(" (")[0]
|
|
230
|
+
return (
|
|
231
|
+
f"Decision {k} (requested at update {r:,}) was not answered in time: the run stopped waiting for it "
|
|
232
|
+
f"after {prev.get('seconds') or 0:.0f} s ({why}). If you answered it, your answer arrived after the run "
|
|
233
|
+
"had stopped waiting and was discarded, and this folder was reset to the running programs."
|
|
234
|
+
)
|
|
235
|
+
if prev.get("backend_error") is not None:
|
|
236
|
+
why = " ".join(str(prev["backend_error"]).split())[:200] or "no error text"
|
|
237
|
+
return (
|
|
238
|
+
f"Decision {k} was not answered: the agent call failed ({why}). The programs were not changed (requested "
|
|
239
|
+
f"at update {r:,})."
|
|
240
|
+
)
|
|
241
|
+
if kind == "rejected":
|
|
242
|
+
return (
|
|
243
|
+
f"Decision {k}: your edit was not applied and was discarded at update {u:,} (the errors below say why); "
|
|
244
|
+
"this folder holds the running programs."
|
|
245
|
+
)
|
|
246
|
+
if kind == "load_failed":
|
|
247
|
+
return (
|
|
248
|
+
f"Decision {k}: your new programs failed to load at update {u:,} and were discarded (see the errors "
|
|
249
|
+
"below); this folder holds the running programs."
|
|
250
|
+
)
|
|
251
|
+
return f"Decision {k} (requested at update {r:,}, answered by update {u:,}): you kept the programs."
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
DEV_SHOWN = 50 # development measurements written into a message; all of them are in the inbox
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def development_line(k, dev) -> str:
|
|
258
|
+
"""The development-set history, oldest first: 'update: loss, accuracy' for the most recent DEV_SHOWN."""
|
|
259
|
+
head = "Development set (held out from training; not the test set in the goal)"
|
|
260
|
+
if not dev:
|
|
261
|
+
return head + ": no measurement yet."
|
|
262
|
+
keys = [x for x in ("loss", "accuracy") if any(x in d for d in dev)]
|
|
263
|
+
shown = dev[-DEV_SHOWN:]
|
|
264
|
+
which = (
|
|
265
|
+
f"all {len(dev)} measurements so far"
|
|
266
|
+
if len(shown) == len(dev)
|
|
267
|
+
else f"the most recent {len(shown)} of {len(dev)} measurements (all in inbox/decision-{k:03d}.json)"
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
def entry(d):
|
|
271
|
+
return f"{d['update']:,}: " + ", ".join(f"{d[x]:.4g}" if x in d else "-" for x in keys)
|
|
272
|
+
|
|
273
|
+
return f"{head}, {' and '.join(keys)} at each evaluation, {which} (update: {', '.join(keys)}): " + "; ".join(
|
|
274
|
+
entry(d) for d in shown
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def format_message(k, req) -> str:
|
|
279
|
+
u, total = req["update"], req["total_updates"]
|
|
280
|
+
ep = f", epoch {u / total * req['epochs']:.2f} of {req['epochs']:g}" if req.get("epochs") else "" # epochs= given
|
|
281
|
+
lines = [
|
|
282
|
+
f"Decision {k}. Requested at update {u:,} of {total:,} ({100 * u / total:.1f}% of training{ep}), after that "
|
|
283
|
+
"update's backward pass and before its parameter update."
|
|
284
|
+
]
|
|
285
|
+
if req["blocking"]:
|
|
286
|
+
lines.append(f"Training is paused until you finish; your edits take effect from update {u:,}.")
|
|
287
|
+
else:
|
|
288
|
+
lines.append(
|
|
289
|
+
"Training continues while you work. Your edits take effect at the first update boundary after you finish; "
|
|
290
|
+
"the next message tells you which update that was."
|
|
291
|
+
)
|
|
292
|
+
lines.append("Why now: " + "; ".join(req["reasons"]) + ".")
|
|
293
|
+
if req.get("last"): # max_decisions reached with this one
|
|
294
|
+
lines.append("This is your last decision in this run.")
|
|
295
|
+
elif "next_decision" in req: # show_next_decision=True
|
|
296
|
+
nxt = req["next_decision"]
|
|
297
|
+
lines.append(f"The next scheduled decision is at update {nxt:,}." if nxt else "No scheduled decisions remain.")
|
|
298
|
+
previous = previous_answer(req.get("previous"), req.get("last_applied"))
|
|
299
|
+
if previous:
|
|
300
|
+
lines.append(previous)
|
|
301
|
+
loss, per = _chunks(req["inbox"]["losses"])
|
|
302
|
+
if loss:
|
|
303
|
+
lines.append(
|
|
304
|
+
f"Training loss since your previous decision (mean per {per} update{'s' if per > 1 else ''}, {len(loss)} "
|
|
305
|
+
f"values, from update {req['inbox']['losses_from']:,}): " + ", ".join(f"{v:.4g}" for v in loss)
|
|
306
|
+
)
|
|
307
|
+
else:
|
|
308
|
+
lines.append("No training loss was passed to step(loss=...) since your previous decision.")
|
|
309
|
+
if "development" in req: # wrap(validation=N): OPAL 0.1.1-validation
|
|
310
|
+
lines.append(development_line(k, req["development"]))
|
|
311
|
+
reports = req["inbox"]["reports"]
|
|
312
|
+
made = max(req["inbox"].get("reports_made", len(reports)), len(reports))
|
|
313
|
+
lines.append(
|
|
314
|
+
f"Reports from your analysis since your previous decision: {made} (all in inbox/decision-{k:03d}.json)."
|
|
315
|
+
if made == len(reports)
|
|
316
|
+
else f"Reports from your analysis since your previous decision: {made}; the most recent {len(reports)} are "
|
|
317
|
+
f"in inbox/decision-{k:03d}.json."
|
|
318
|
+
)
|
|
319
|
+
if reports:
|
|
320
|
+
lines.append("Most recent report: " + json.dumps(reports[-1], default=str)[:6000])
|
|
321
|
+
if req["inbox"].get("metrics"):
|
|
322
|
+
lines.append(
|
|
323
|
+
"Values the training script chose to show you (most recent 20): "
|
|
324
|
+
+ json.dumps(req["inbox"]["metrics"][-20:], default=str)[:3000]
|
|
325
|
+
)
|
|
326
|
+
skipped = req["inbox"].get("skipped") or []
|
|
327
|
+
if skipped:
|
|
328
|
+
shown = ", ".join(f"{u:,}" for u in skipped[:20]) + (", ..." if len(skipped) > 20 else "")
|
|
329
|
+
lines.append(f"Updates your program skipped since your previous decision: {len(skipped)} ({shown}).")
|
|
330
|
+
errs = req["inbox"]["errors"]
|
|
331
|
+
if errs:
|
|
332
|
+
lines.append(
|
|
333
|
+
"Errors since your previous decision:\n"
|
|
334
|
+
+ "\n".join(f"- update {e['update']} in {e['where']}:\n{e['error']}" for e in errs[-10:])
|
|
335
|
+
)
|
|
336
|
+
else:
|
|
337
|
+
lines.append("No errors since your previous decision.")
|
|
338
|
+
t = req["timing"]
|
|
339
|
+
if t.get("wall_ms_per_update") is not None:
|
|
340
|
+
lines.append(
|
|
341
|
+
f"Timing: {t['wall_ms_per_update']:.1f} ms of wall time per training update recently; your updater "
|
|
342
|
+
f"{_ms(t.get('program_ms'))} per update (the starting program: {_ms(t.get('program_ms_start'))})."
|
|
343
|
+
)
|
|
344
|
+
lines.append(
|
|
345
|
+
f"Profile of one update: your updater, {audit.describe(t.get('audit'))}; the starting program, "
|
|
346
|
+
f"{audit.describe(t.get('audit_start'))}."
|
|
347
|
+
+ (
|
|
348
|
+
f" Your analysis took {_ms(t['analysis_ms'])} at its last call."
|
|
349
|
+
if t.get("analysis_ms") is not None
|
|
350
|
+
else ""
|
|
351
|
+
)
|
|
352
|
+
)
|
|
353
|
+
if req.get("notes"):
|
|
354
|
+
lines.append("Note from the person running this training: " + req["notes"])
|
|
355
|
+
so_far = f"Decisions so far: {k - 1}; scheduled decisions left: {req['scheduled_left']}"
|
|
356
|
+
neutral = req.get("neutral") # the neutral prompts: no cost
|
|
357
|
+
lines.append(so_far + ("." if neutral else f"; agent cost so far: ${req['cost_so_far']:.2f}."))
|
|
358
|
+
lines.append(
|
|
359
|
+
"Your programs are in this folder ("
|
|
360
|
+
+ ", ".join(sorted(req["files"]))
|
|
361
|
+
+ "); inbox/ holds every input so far"
|
|
362
|
+
+ (
|
|
363
|
+
(
|
|
364
|
+
" and reference/ read-only copies of the training script and user code"
|
|
365
|
+
if req.get("script_shared", True)
|
|
366
|
+
else " and reference/ read-only copies of user code"
|
|
367
|
+
)
|
|
368
|
+
if req.get("shared_code", True)
|
|
369
|
+
else ""
|
|
370
|
+
)
|
|
371
|
+
+ "; put scratch scripts in scratch/ (only "
|
|
372
|
+
"updater.py, analysis.py and the modules they import are the program). "
|
|
373
|
+
+ (aggressive.EDIT_SENTENCE if req.get("aggressive") else "Edit the programs in place, or keep them (a valid decision). ")
|
|
374
|
+
+ (
|
|
375
|
+
aggressive.RATIONALE_SENTENCE
|
|
376
|
+
if req.get("aggressive") # OPAL 0.1.2-aggressive
|
|
377
|
+
else "End with your rationale in the four parts of DECISIONS: observed, changed or kept, prediction, rules."
|
|
378
|
+
if req.get("neutral") # the neutral prompt
|
|
379
|
+
else "End with a brief rationale: what you observed, what you changed or kept, the effect you predict, and "
|
|
380
|
+
"whether it is a fixed schedule or a feedback rule."
|
|
381
|
+
)
|
|
382
|
+
)
|
|
383
|
+
return "\n".join(lines)
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def now_stamp():
|
|
387
|
+
return time.strftime("%Y%m%d-%H%M%S")
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def new_run_dir(root: Path) -> Path:
|
|
391
|
+
"""A fresh run folder: time, process id and a random suffix, so runs started in the same second never share one."""
|
|
392
|
+
path = Path(root) / f"{time.strftime('%Y%m%d-%H%M%S')}-{os.getpid()}-{secrets.token_hex(2)}"
|
|
393
|
+
path.mkdir(parents=True, exist_ok=False)
|
|
394
|
+
return path
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Agent backends. Each one runs one decision: the agent reads and edits the files in `workspace` and answers in text.
|
|
2
|
+
|
|
3
|
+
decide(workspace, message, system, record_dir) -> dict(text, cost_usd, returncode, error)
|
|
4
|
+
`system` is the system prompt, passed every time; each backend keeps one persistent conversation across decisions.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from .base import Backend, CallableBackend, KeepBackend
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def make_backend(spec, **kw) -> Backend:
|
|
13
|
+
if isinstance(spec, Backend):
|
|
14
|
+
return spec
|
|
15
|
+
if callable(spec):
|
|
16
|
+
return CallableBackend(spec)
|
|
17
|
+
if spec == "keep":
|
|
18
|
+
return KeepBackend()
|
|
19
|
+
if spec == "claude":
|
|
20
|
+
from .claude import ClaudeBackend
|
|
21
|
+
|
|
22
|
+
return ClaudeBackend(**kw)
|
|
23
|
+
if spec == "codex":
|
|
24
|
+
from .codex import CodexBackend
|
|
25
|
+
|
|
26
|
+
return CodexBackend(**kw)
|
|
27
|
+
if isinstance(spec, str) and spec.startswith("mailbox:"):
|
|
28
|
+
from .mailbox import MailboxBackend
|
|
29
|
+
|
|
30
|
+
return MailboxBackend(spec, **kw)
|
|
31
|
+
raise ValueError(f"unknown backend {spec!r}: use 'claude', 'codex', 'mailbox:<dir>', 'keep', or a callable")
|