haliosai-cli 2.0.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.
@@ -0,0 +1,308 @@
1
+ """Prompt-optimization control plane for coding agents."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import pathlib
7
+ from typing import Any
8
+
9
+ import typer
10
+
11
+ from .cli_support import ApiClient, atomic_write_text, load_project_config, resolve_credentials
12
+
13
+ app = typer.Typer(
14
+ help="Guide, record, and verify coding-agent prompt optimization.",
15
+ no_args_is_help=True,
16
+ )
17
+
18
+
19
+ def _context() -> tuple[str, Any]:
20
+ _root, config = load_project_config()
21
+ agent_id = str((config.get("agent") or {}).get("id") or "")
22
+ credentials = resolve_credentials(str(config.get("profile") or "default"), agent_id)
23
+ return agent_id, credentials
24
+
25
+
26
+ def _emit(value: dict[str, Any], json_output: bool) -> None:
27
+ if json_output:
28
+ typer.echo(json.dumps(value, indent=2, sort_keys=True, default=str))
29
+ else:
30
+ typer.echo(json.dumps(value, indent=2, default=str))
31
+
32
+
33
+ def _resolve_baseline(api: ApiClient, agent_id: str, explicit_run_id: str | None) -> dict[str, Any]:
34
+ if explicit_run_id:
35
+ baseline = api.request("GET", f"/api/v1/runs/evaluations/{explicit_run_id}")
36
+ else:
37
+ listing = api.request(
38
+ "GET", "/api/v1/runs/evaluations", params={"agent_id": agent_id, "limit": 20}
39
+ )
40
+ baseline = next(
41
+ (
42
+ item
43
+ for item in listing.get("items") or []
44
+ if item.get("status") in {"completed", "failed"}
45
+ and int(item.get("attempted_trial_count") or 0) > 0
46
+ and int(item.get("telemetry_incomplete_count") or 0) == 0
47
+ ),
48
+ None,
49
+ )
50
+ if baseline is None:
51
+ raise typer.BadParameter(
52
+ "No complete canonical eval run is available; run `halios eval run` first"
53
+ )
54
+ baseline = api.request("GET", f"/api/v1/runs/evaluations/{baseline['run_id']}")
55
+ if baseline.get("status") not in {"completed", "failed"}:
56
+ raise typer.BadParameter("Optimization baseline must be a complete evaluation run")
57
+ if int(baseline.get("telemetry_incomplete_count") or 0):
58
+ raise typer.BadParameter("Optimization baseline has incomplete telemetry")
59
+ return baseline
60
+
61
+
62
+ def _scorecard(report: dict[str, Any]) -> dict[str, Any]:
63
+ return {
64
+ "overall_score": float(report.get("pass_at_k") or 0),
65
+ "gate_passed": bool(report.get("gate_passed")),
66
+ "protected_failure": bool(report.get("protected_failure")),
67
+ "telemetry_incomplete_count": int(report.get("telemetry_incomplete_count") or 0),
68
+ "check_execution_error_count": int(report.get("check_execution_error_count") or 0),
69
+ "attempted_trial_count": int(report.get("attempted_trial_count") or 0),
70
+ "suite_digest": report.get("suite_digest"),
71
+ }
72
+
73
+
74
+ def _safe_gate(report: dict[str, Any]) -> bool:
75
+ return (
76
+ bool(report.get("gate_passed"))
77
+ and not bool(report.get("protected_failure"))
78
+ and not (
79
+ int(report.get("telemetry_incomplete_count") or 0)
80
+ or int(report.get("check_execution_error_count") or 0)
81
+ )
82
+ )
83
+
84
+
85
+ @app.command("start")
86
+ def start(
87
+ prompt_file: pathlib.Path = typer.Option(..., "--prompt-file", exists=True, dir_okay=False),
88
+ baseline_run: str | None = typer.Option(None, "--baseline-run"),
89
+ name: str = typer.Option("coding-agent-optimization", "--name"),
90
+ max_iterations: int = typer.Option(5, "--max-iterations", min=1, max=20),
91
+ max_character_delta: int = typer.Option(300, "--max-character-delta", min=1, max=5000),
92
+ json_output: bool = typer.Option(False, "--json"),
93
+ ) -> None:
94
+ """Freeze a canonical baseline and open a coding-agent optimization run."""
95
+ agent_id, credentials = _context()
96
+ starting_prompt = prompt_file.read_text(encoding="utf-8")
97
+ if not starting_prompt.strip():
98
+ raise typer.BadParameter("--prompt-file must contain the current system prompt")
99
+ with ApiClient(credentials) as api:
100
+ baseline = _resolve_baseline(api, agent_id, baseline_run)
101
+ created = api.request(
102
+ "POST",
103
+ "/api/v1/optimization-runs",
104
+ json={
105
+ "agent_id": agent_id,
106
+ "name": name,
107
+ "strategy": "simple",
108
+ "starting_prompt": starting_prompt,
109
+ "baseline_run_id": baseline["run_id"],
110
+ "config": {
111
+ "stopping": {
112
+ "max_iterations": max_iterations,
113
+ "max_character_delta": max_character_delta,
114
+ },
115
+ "preflight": {"skip": True},
116
+ "require_t1_gate": True,
117
+ "minimum_improvement": 0.000001,
118
+ },
119
+ },
120
+ )
121
+ run_id = str(created["id"])
122
+ api.request("POST", f"/api/v1/optimization-runs/{run_id}/start")
123
+ scorecard = _scorecard(baseline)
124
+ api.request(
125
+ "POST",
126
+ f"/api/v1/optimization-runs/{run_id}/iterations",
127
+ json={
128
+ "iteration_number": 0,
129
+ "verdict": "baseline",
130
+ "harness_verdict": "baseline",
131
+ "prompt_before": starting_prompt,
132
+ "prompt_after": starting_prompt,
133
+ "scorecard_json": scorecard,
134
+ "scorecard_delta_json": {"delta": 0.0, "check_deltas": {}},
135
+ "t1_gate_passed": _safe_gate(baseline),
136
+ "trace_run_tag": baseline.get("run_tag"),
137
+ },
138
+ )
139
+ guidance = api.request("POST", f"/api/v1/optimization-runs/{run_id}/next-action")
140
+ result = {
141
+ "optimization_run_id": run_id,
142
+ "baseline_run_id": baseline["run_id"],
143
+ "baseline_scorecard": scorecard,
144
+ "prompt_file": str(prompt_file.resolve()),
145
+ "guidance": guidance,
146
+ "next": (
147
+ "Make one focused prompt edit within the mutation contract, run the unchanged "
148
+ "canonical suite with `halios eval run --json`, then record it with "
149
+ f"`halios optimize record {run_id} --evaluation-run <run-id> "
150
+ f"--prompt-file {prompt_file}`."
151
+ ),
152
+ }
153
+ _emit(result, json_output)
154
+
155
+
156
+ @app.command("guidance")
157
+ def guidance(run_id: str, json_output: bool = typer.Option(False, "--json")) -> None:
158
+ """Return the next bounded edit contract and negative memory for a coding agent."""
159
+ _agent_id, credentials = _context()
160
+ with ApiClient(credentials) as api:
161
+ result = api.request("POST", f"/api/v1/optimization-runs/{run_id}/next-action")
162
+ _emit(result, json_output)
163
+
164
+
165
+ @app.command("record")
166
+ def record(
167
+ run_id: str,
168
+ evaluation_run_id: str = typer.Option(..., "--evaluation-run"),
169
+ prompt_file: pathlib.Path = typer.Option(..., "--prompt-file", exists=True, dir_okay=False),
170
+ json_output: bool = typer.Option(False, "--json"),
171
+ ) -> None:
172
+ """Record one candidate using evidence from the unchanged canonical eval suite."""
173
+ _agent_id, credentials = _context()
174
+ prompt_after = prompt_file.read_text(encoding="utf-8")
175
+ with ApiClient(credentials) as api:
176
+ run = api.request("GET", f"/api/v1/optimization-runs/{run_id}")
177
+ report = api.request("GET", f"/api/v1/runs/evaluations/{evaluation_run_id}")
178
+ if report.get("status") not in {"completed", "failed"}:
179
+ raise typer.BadParameter("Candidate evaluation run is not complete")
180
+ baseline = api.request(
181
+ "GET", f"/api/v1/runs/evaluations/{run['baseline_evaluation_run_id']}"
182
+ )
183
+ if report.get("suite_digest") != baseline.get("suite_digest"):
184
+ raise typer.BadParameter("Candidate evaluation suite differs from the frozen baseline")
185
+ if int(report.get("attempted_trial_count") or 0) != int(
186
+ baseline.get("attempted_trial_count") or 0
187
+ ):
188
+ raise typer.BadParameter("Candidate trial count differs from the frozen baseline")
189
+ iterations = run.get("iterations") or []
190
+ candidates = [item for item in iterations if int(item.get("iteration_number") or 0) > 0]
191
+ iteration_number = len(candidates) + 1
192
+ prompt_before = str(run.get("current_prompt") or run.get("starting_prompt") or "")
193
+ scorecard = _scorecard(report)
194
+ delta = scorecard["overall_score"] - float(baseline.get("pass_at_k") or 0)
195
+ iteration = api.request(
196
+ "POST",
197
+ f"/api/v1/optimization-runs/{run_id}/iterations",
198
+ json={
199
+ "iteration_number": iteration_number,
200
+ "verdict": "accept" if delta > 0 and _safe_gate(report) else "discard",
201
+ "harness_verdict": "accept" if delta > 0 and _safe_gate(report) else "discard",
202
+ "prompt_before": prompt_before,
203
+ "prompt_after": prompt_after,
204
+ "scorecard_json": scorecard,
205
+ "scorecard_delta_json": {"delta": delta, "check_deltas": {}},
206
+ "t1_gate_passed": _safe_gate(report),
207
+ "trace_run_tag": report.get("run_tag"),
208
+ },
209
+ )
210
+ accepted = iteration.get("backend_verdict") == "accept"
211
+ if accepted:
212
+ api.request(
213
+ "PATCH",
214
+ f"/api/v1/optimization-runs/{run_id}",
215
+ json={"status": "complete", "accepted_iteration_id": iteration["id"]},
216
+ )
217
+ next_action = (
218
+ None
219
+ if accepted
220
+ else api.request("POST", f"/api/v1/optimization-runs/{run_id}/next-action")
221
+ )
222
+ result = {
223
+ "optimization_run_id": run_id,
224
+ "iteration": iteration,
225
+ "accepted": accepted,
226
+ "next_action": next_action,
227
+ "next": (
228
+ f"halios optimize apply {iteration['id']} --output {prompt_file} --json"
229
+ if accepted
230
+ else "Revert the rejected prompt edit, inspect next_action, and try one different edit."
231
+ ),
232
+ }
233
+ _emit(result, json_output)
234
+ if not accepted:
235
+ raise typer.Exit(2)
236
+
237
+
238
+ @app.command("apply")
239
+ def apply_candidate(
240
+ candidate_id: str,
241
+ output: pathlib.Path | None = typer.Option(None, "--output", dir_okay=False),
242
+ json_output: bool = typer.Option(False, "--json"),
243
+ ) -> None:
244
+ """Retrieve one backend-approved prompt candidate for repository application."""
245
+ _agent_id, credentials = _context()
246
+ with ApiClient(credentials) as api:
247
+ handoff = api.request(
248
+ "POST",
249
+ "/api/v1/optimization-runs/candidate-handoff",
250
+ json={"candidate_id": candidate_id},
251
+ )
252
+ prompt = str(handoff["prompt"])
253
+ if output:
254
+ atomic_write_text(output, prompt)
255
+ handoff["output_path"] = str(output.resolve())
256
+ handoff["next"] = (
257
+ "Run `halios eval run --json` after applying the prompt, then use "
258
+ f"`halios optimize verify {handoff['optimization_run_id']} --evaluation-run <run-id>`."
259
+ )
260
+ if json_output:
261
+ _emit(handoff, True)
262
+ elif output:
263
+ typer.echo(f"Wrote backend-approved prompt to {output.resolve()}")
264
+ else:
265
+ typer.echo(prompt)
266
+
267
+
268
+ @app.command("verify")
269
+ def verify_candidate(
270
+ run_id: str,
271
+ evaluation_run_id: str = typer.Option(..., "--evaluation-run"),
272
+ json_output: bool = typer.Option(False, "--json"),
273
+ ) -> None:
274
+ """Verify the applied candidate against the frozen baseline and unchanged suite."""
275
+ _agent_id, credentials = _context()
276
+ with ApiClient(credentials) as api:
277
+ result = api.request(
278
+ "POST",
279
+ f"/api/v1/optimization-runs/{run_id}/verify",
280
+ json={"evaluation_run_id": evaluation_run_id},
281
+ )
282
+ _emit(result, json_output)
283
+ if not result.get("passed"):
284
+ raise typer.Exit(2)
285
+
286
+
287
+ @app.command("list")
288
+ def list_runs(json_output: bool = typer.Option(False, "--json")) -> None:
289
+ agent_id, credentials = _context()
290
+ with ApiClient(credentials) as api:
291
+ result = api.request("GET", "/api/v1/optimization-runs", params={"agent_id": agent_id})
292
+ _emit(result, json_output)
293
+
294
+
295
+ @app.command("status")
296
+ def status(run_id: str, json_output: bool = typer.Option(False, "--json")) -> None:
297
+ _agent_id, credentials = _context()
298
+ with ApiClient(credentials) as api:
299
+ result = api.request("GET", f"/api/v1/optimization-runs/{run_id}")
300
+ _emit(result, json_output)
301
+
302
+
303
+ @app.command("cancel")
304
+ def cancel(run_id: str, json_output: bool = typer.Option(False, "--json")) -> None:
305
+ _agent_id, credentials = _context()
306
+ with ApiClient(credentials) as api:
307
+ result = api.request("POST", f"/api/v1/optimization-runs/{run_id}/cancel")
308
+ _emit(result, json_output)