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.
- halios_cli/__init__.py +5 -0
- halios_cli/_version.py +1 -0
- halios_cli/cli.py +47 -0
- halios_cli/cli_auth.py +162 -0
- halios_cli/cli_eval.py +1075 -0
- halios_cli/cli_optimize.py +308 -0
- halios_cli/cli_project.py +404 -0
- halios_cli/cli_scenario.py +96 -0
- halios_cli/cli_support.py +373 -0
- halios_cli/cli_trace.py +175 -0
- halios_cli/py.typed +1 -0
- halios_cli/schemas/__init__.py +1 -0
- halios_cli/schemas/eval.schema.json +105 -0
- halios_cli/schemas/scenarios.schema.json +96 -0
- haliosai_cli-2.0.0.dist-info/METADATA +101 -0
- haliosai_cli-2.0.0.dist-info/RECORD +20 -0
- haliosai_cli-2.0.0.dist-info/WHEEL +5 -0
- haliosai_cli-2.0.0.dist-info/entry_points.txt +2 -0
- haliosai_cli-2.0.0.dist-info/licenses/LICENSE +200 -0
- haliosai_cli-2.0.0.dist-info/top_level.txt +1 -0
|
@@ -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)
|