convergent-cli 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.
Potentially problematic release.
This version of convergent-cli might be problematic. Click here for more details.
- convergent_cli/__init__.py +1 -0
- convergent_cli/auth.py +227 -0
- convergent_cli/build_context.py +225 -0
- convergent_cli/calibration_finish.py +310 -0
- convergent_cli/calibration_frontier.py +499 -0
- convergent_cli/calibration_runner.py +1413 -0
- convergent_cli/calibration_send.py +44 -0
- convergent_cli/codex_app_server.py +127 -0
- convergent_cli/command_tree.py +102 -0
- convergent_cli/commands/__init__.py +58 -0
- convergent_cli/commands/agents.py +729 -0
- convergent_cli/commands/auth.py +117 -0
- convergent_cli/commands/calibration.py +3857 -0
- convergent_cli/commands/calibration_labels_load.py +627 -0
- convergent_cli/commands/capabilities.py +85 -0
- convergent_cli/commands/dataset_authoring.py +964 -0
- convergent_cli/commands/dataset_import.py +763 -0
- convergent_cli/commands/datasets.py +678 -0
- convergent_cli/commands/docs.py +120 -0
- convergent_cli/commands/doctor.py +124 -0
- convergent_cli/commands/environments.py +63 -0
- convergent_cli/commands/findings.py +206 -0
- convergent_cli/commands/graders.py +803 -0
- convergent_cli/commands/harness.py +366 -0
- convergent_cli/commands/issues.py +388 -0
- convergent_cli/commands/production.py +173 -0
- convergent_cli/commands/profile.py +86 -0
- convergent_cli/commands/rubrics.py +14 -0
- convergent_cli/commands/runs.py +1763 -0
- convergent_cli/commands/setup.py +968 -0
- convergent_cli/commands/task_qc.py +1041 -0
- convergent_cli/commands/task_qc_reports.py +393 -0
- convergent_cli/commands/verdicts.py +156 -0
- convergent_cli/commands/workspaces.py +170 -0
- convergent_cli/commands/worlds.py +292 -0
- convergent_cli/confirm.py +59 -0
- convergent_cli/control_client.py +141 -0
- convergent_cli/customer_client.py +1272 -0
- convergent_cli/docs_client.py +165 -0
- convergent_cli/errors.py +212 -0
- convergent_cli/harness_run.py +1169 -0
- convergent_cli/harness_tree.py +854 -0
- convergent_cli/installed_agent.py +1071 -0
- convergent_cli/json_files.py +23 -0
- convergent_cli/local_files.py +64 -0
- convergent_cli/main.py +106 -0
- convergent_cli/oauth.py +431 -0
- convergent_cli/output.py +15 -0
- convergent_cli/paging.py +62 -0
- convergent_cli/run_calibration.py +361 -0
- convergent_cli/run_usage.py +99 -0
- convergent_cli/sessions.py +275 -0
- convergent_cli/setup_agents.py +190 -0
- convergent_cli/setup_auth.py +463 -0
- convergent_cli/setup_instrument.py +244 -0
- convergent_cli/setup_mcp.py +525 -0
- convergent_cli/setup_wizard.py +125 -0
- convergent_cli/skill_files/convergent/SKILL.md +25 -0
- convergent_cli/skill_files/convergent-instrument/SKILL.md +15 -0
- convergent_cli/skill_files/convergent-verify/SKILL.md +15 -0
- convergent_cli/skills.py +489 -0
- convergent_cli/state.py +297 -0
- convergent_cli/transport.py +242 -0
- convergent_cli/verdict_feedback.py +161 -0
- convergent_cli/verifier_bench_preflight.py +32 -0
- convergent_cli/world_docker.py +145 -0
- convergent_cli/world_files.py +432 -0
- convergent_cli/world_live.py +660 -0
- convergent_cli/world_mcp.py +109 -0
- convergent_cli/world_probe_requirements.txt +29 -0
- convergent_cli/world_template/Dockerfile +10 -0
- convergent_cli/world_template/README.md +38 -0
- convergent_cli/world_template/convergent.toml +11 -0
- convergent_cli/world_template/examples/default/environment/catalog.json +3 -0
- convergent_cli/world_template/examples/default/instruction.md +1 -0
- convergent_cli/world_template/requirements.txt +1 -0
- convergent_cli/world_template/schemas/catalog.json +5 -0
- convergent_cli/world_template/server.py +25 -0
- convergent_cli-0.1.0.dist-info/METADATA +679 -0
- convergent_cli-0.1.0.dist-info/RECORD +83 -0
- convergent_cli-0.1.0.dist-info/WHEEL +4 -0
- convergent_cli-0.1.0.dist-info/entry_points.txt +2 -0
- convergent_cli-0.1.0.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
"""Validate recorded treatment identity, declare it, then finish each async send."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import time
|
|
7
|
+
from collections.abc import Mapping
|
|
8
|
+
from concurrent.futures import FIRST_COMPLETED, CancelledError, Future, wait
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from threading import Event, Thread
|
|
11
|
+
|
|
12
|
+
from convergent_cli import calibration_runner as runner
|
|
13
|
+
from convergent_cli.calibration_send import SendDeadline, SendTimeout
|
|
14
|
+
from convergent_cli.commands import calibration as cal
|
|
15
|
+
from convergent_cli.confirm import confirm_write
|
|
16
|
+
from convergent_cli.customer_client import ControlPlaneRefusedError
|
|
17
|
+
from convergent_cli.errors import ExitCode, UsageError
|
|
18
|
+
from convergent_cli.output import emit_json
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class _Run:
|
|
23
|
+
tenant: cal._ManifestTenant
|
|
24
|
+
source: str
|
|
25
|
+
run_id: str
|
|
26
|
+
recorded: Mapping[str, object]
|
|
27
|
+
gate_run_id: str | None = None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def concurrency(value: str) -> int:
|
|
31
|
+
try:
|
|
32
|
+
bound = int(value)
|
|
33
|
+
except ValueError as exc:
|
|
34
|
+
raise argparse.ArgumentTypeError("--concurrency must be a positive integer") from exc
|
|
35
|
+
if bound < 1:
|
|
36
|
+
raise argparse.ArgumentTypeError("--concurrency must be a positive integer")
|
|
37
|
+
return bound
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def finish(args: argparse.Namespace) -> int:
|
|
41
|
+
treatment = runner.require_treatment(args.treatment)
|
|
42
|
+
k = runner.require_k(args.k)
|
|
43
|
+
pins = cal._parsed_pins(args, None)
|
|
44
|
+
label = cal._input_label(args)
|
|
45
|
+
named = cal._optional_digest(args.preprocessing_digest)
|
|
46
|
+
gate, gate_pins = cal._gate_choice(args, treatment)
|
|
47
|
+
manifest = cal._load_manifest(runner.require_manifest_id(args.corpus_manifest_id))
|
|
48
|
+
cal._require_scoped_preprocessing(manifest, named, "--preprocessing-digest")
|
|
49
|
+
cal._refuse_treatment_bound_elsewhere(treatment, manifest, k, pins, label, named)
|
|
50
|
+
runs: list[_Run] = []
|
|
51
|
+
for tenant in manifest.included:
|
|
52
|
+
source, run_id, recorded = cal._resolve_treatment_run(
|
|
53
|
+
treatment, manifest, tenant, k, pins, label, named
|
|
54
|
+
)
|
|
55
|
+
if recorded is None:
|
|
56
|
+
raise UsageError(f"treatment_run_missing: {tenant.tenant_tag}")
|
|
57
|
+
runs.append(_Run(tenant, source, run_id, recorded))
|
|
58
|
+
if not runs:
|
|
59
|
+
raise UsageError("treatment_runs_missing: no included tenants")
|
|
60
|
+
_settle(runs, wait=args.wait)
|
|
61
|
+
runtime, digest, suite = _identity(runs, k, pins)
|
|
62
|
+
if runner.runtime_has_decider(runtime) and gate is None and not args.ungated:
|
|
63
|
+
raise UsageError(
|
|
64
|
+
"gate_choice_required: pass --gate-baseline TREATMENT or --ungated. "
|
|
65
|
+
"The first send is final."
|
|
66
|
+
)
|
|
67
|
+
for run in runs:
|
|
68
|
+
tenant = run.tenant
|
|
69
|
+
cal._refuse_mismatch(
|
|
70
|
+
run.recorded,
|
|
71
|
+
tenant,
|
|
72
|
+
dataset_id=tenant.dataset_id,
|
|
73
|
+
version_id=tenant.version_id,
|
|
74
|
+
samples=k,
|
|
75
|
+
preprocessing_digest=cal._active_digest(tenant, named),
|
|
76
|
+
criterion_versions=pins,
|
|
77
|
+
treatment=treatment,
|
|
78
|
+
tenant_tag=tenant.tenant_tag,
|
|
79
|
+
manifest_id=manifest.manifest_id,
|
|
80
|
+
label_release=manifest.label_release,
|
|
81
|
+
source_run_id=run.source,
|
|
82
|
+
)
|
|
83
|
+
if gate is not None:
|
|
84
|
+
run.gate_run_id = cal._treatment_id(
|
|
85
|
+
gate,
|
|
86
|
+
manifest,
|
|
87
|
+
tenant,
|
|
88
|
+
k,
|
|
89
|
+
run.source,
|
|
90
|
+
gate_pins,
|
|
91
|
+
labeled=label is not None,
|
|
92
|
+
input_base=cal._input_base(manifest, tenant, label, named),
|
|
93
|
+
preprocessing_digest=named,
|
|
94
|
+
)
|
|
95
|
+
baseline = cal._get_run(tenant, run.gate_run_id)
|
|
96
|
+
if baseline is None or baseline.get("status") != "succeeded":
|
|
97
|
+
raise UsageError(f"gate_baseline_unavailable: {tenant.tenant_tag}")
|
|
98
|
+
confirm_write("This will declare the treatment and send each tenant's contribution.")
|
|
99
|
+
# The declaration endpoint compares the complete identity and is a no-op
|
|
100
|
+
# for an identical declaration. Revalidate even when every send is stored.
|
|
101
|
+
declaration_digest = cal._declaration_preprocessing_digest(manifest, named)
|
|
102
|
+
runtime = cal._resolved_declaration_runtime(
|
|
103
|
+
runs[0].recorded, runtime, runs[0].tenant.tenant_tag
|
|
104
|
+
)
|
|
105
|
+
cal._write_declaration(
|
|
106
|
+
treatment, manifest.manifest_id, [suite], runtime, k, 1000, declaration_digest
|
|
107
|
+
)
|
|
108
|
+
rows: list[dict[str, object]] = [
|
|
109
|
+
{"tenant_tag": run.tenant.tenant_tag, "declared": True, "sent": "not_sent", "reason": None}
|
|
110
|
+
for run in runs
|
|
111
|
+
]
|
|
112
|
+
pending: dict[Future[dict[str, object]], int] = {}
|
|
113
|
+
remaining = iter(enumerate(runs))
|
|
114
|
+
stopped = Event()
|
|
115
|
+
try:
|
|
116
|
+
for index, run in remaining:
|
|
117
|
+
pending[_start_send(run, treatment, args, stopped)] = index
|
|
118
|
+
rows[index]["sent"] = "sending"
|
|
119
|
+
if len(pending) == args.concurrency:
|
|
120
|
+
break
|
|
121
|
+
while pending:
|
|
122
|
+
done, _ = wait(pending, return_when=FIRST_COMPLETED)
|
|
123
|
+
# Read every completed result before starting another tenant.
|
|
124
|
+
for future in done:
|
|
125
|
+
rows[pending[future]] = future.result()
|
|
126
|
+
for future in done:
|
|
127
|
+
del pending[future]
|
|
128
|
+
for index, run in remaining:
|
|
129
|
+
pending[_start_send(run, treatment, args, stopped)] = index
|
|
130
|
+
rows[index]["sent"] = "sending"
|
|
131
|
+
if len(pending) == args.concurrency:
|
|
132
|
+
break
|
|
133
|
+
for row in rows:
|
|
134
|
+
if args.format != "json":
|
|
135
|
+
state, reason = row["sent"], row["reason"]
|
|
136
|
+
detail = f"{state} {reason}" if reason is not None else state
|
|
137
|
+
unchanged = (
|
|
138
|
+
" (already declared and stored; unchanged)" if state == "duplicate" else ""
|
|
139
|
+
)
|
|
140
|
+
print(f"{row['tenant_tag']}: declared, sent: {detail}{unchanged}")
|
|
141
|
+
if "hint" in row:
|
|
142
|
+
print(row["hint"])
|
|
143
|
+
shown = cal._staff_get(f"{cal._TREATMENTS_PATH}/{cal._quote(treatment)}")
|
|
144
|
+
summary = {
|
|
145
|
+
"complete": shown.get("complete"),
|
|
146
|
+
"tenants": len(cal._mappings(shown.get("tenants"))),
|
|
147
|
+
"grade_once_usd": shown.get("grade_once_usd"),
|
|
148
|
+
}
|
|
149
|
+
except (Exception, KeyboardInterrupt):
|
|
150
|
+
stopped.set()
|
|
151
|
+
for future in pending:
|
|
152
|
+
future.cancel()
|
|
153
|
+
for future, index in pending.items():
|
|
154
|
+
if future.cancelled():
|
|
155
|
+
rows[index]["sent"] = "not_sent"
|
|
156
|
+
elif future.done():
|
|
157
|
+
if future.exception() is None:
|
|
158
|
+
rows[index] = future.result()
|
|
159
|
+
else:
|
|
160
|
+
rows[index].update(sent="error", reason="interrupted")
|
|
161
|
+
if args.format == "json":
|
|
162
|
+
emit_json({"tenants": rows, "summary": None})
|
|
163
|
+
raise
|
|
164
|
+
if args.format == "json":
|
|
165
|
+
emit_json({"tenants": rows, "summary": summary})
|
|
166
|
+
else:
|
|
167
|
+
print(
|
|
168
|
+
f"complete={str(summary['complete']).lower()} tenants={summary['tenants']} "
|
|
169
|
+
f"grade_once_usd={summary['grade_once_usd']}"
|
|
170
|
+
)
|
|
171
|
+
refused = any(row["sent"] in {"refused", "error", "timeout"} for row in rows)
|
|
172
|
+
return int(ExitCode.REMOTE) if refused or summary["complete"] is not True else 0
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _start_send(
|
|
176
|
+
run: _Run, treatment: str, args: argparse.Namespace, stopped: Event
|
|
177
|
+
) -> Future[dict[str, object]]:
|
|
178
|
+
future: Future[dict[str, object]] = Future()
|
|
179
|
+
|
|
180
|
+
def send() -> None:
|
|
181
|
+
if not future.set_running_or_notify_cancel():
|
|
182
|
+
return
|
|
183
|
+
try:
|
|
184
|
+
future.set_result(_send(run, treatment, args, stopped))
|
|
185
|
+
except BaseException as exc:
|
|
186
|
+
future.set_exception(exc)
|
|
187
|
+
|
|
188
|
+
# A blocked HTTP request must not keep the CLI alive during interpreter exit.
|
|
189
|
+
Thread(target=send, daemon=True).start()
|
|
190
|
+
return future
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _send(run: _Run, treatment: str, args: argparse.Namespace, stopped: Event) -> dict[str, object]:
|
|
194
|
+
row: dict[str, object] = {
|
|
195
|
+
"tenant_tag": run.tenant.tenant_tag,
|
|
196
|
+
"declared": True,
|
|
197
|
+
"sent": "sending",
|
|
198
|
+
"reason": None,
|
|
199
|
+
}
|
|
200
|
+
if stopped.is_set():
|
|
201
|
+
row["sent"] = "not_sent"
|
|
202
|
+
return row
|
|
203
|
+
deadline = SendDeadline(args.send_timeout)
|
|
204
|
+
try:
|
|
205
|
+
deadline.remaining()
|
|
206
|
+
state, reason = cal._send_contribution(
|
|
207
|
+
run.tenant,
|
|
208
|
+
run.run_id,
|
|
209
|
+
treatment,
|
|
210
|
+
run.gate_run_id,
|
|
211
|
+
ungated=args.ungated,
|
|
212
|
+
deadline=deadline,
|
|
213
|
+
cancelled=stopped.is_set,
|
|
214
|
+
)
|
|
215
|
+
if state == "stored":
|
|
216
|
+
state = "duplicate"
|
|
217
|
+
row.update(sent=state, reason=reason)
|
|
218
|
+
while state in {"sending", "counting"} and not stopped.is_set():
|
|
219
|
+
deadline.pause()
|
|
220
|
+
if stopped.is_set():
|
|
221
|
+
break
|
|
222
|
+
state, reason = cal._poll_contribution(run.tenant, run.run_id, deadline=deadline)
|
|
223
|
+
row.update(sent=state, reason=reason)
|
|
224
|
+
except CancelledError:
|
|
225
|
+
row.update(sent="not_sent", reason=None)
|
|
226
|
+
except SendTimeout:
|
|
227
|
+
row.update(
|
|
228
|
+
sent="timeout",
|
|
229
|
+
reason="send_timeout",
|
|
230
|
+
hint=f"Check cv calibration show {treatment}.",
|
|
231
|
+
)
|
|
232
|
+
except ControlPlaneRefusedError as exc:
|
|
233
|
+
row.update(sent="refused", reason=str(exc))
|
|
234
|
+
except Exception as exc:
|
|
235
|
+
# Exception text can contain customer response bodies or credentials.
|
|
236
|
+
row.update(sent="error", reason=type(exc).__name__)
|
|
237
|
+
return row
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _settle(runs: list[_Run], *, wait: bool) -> None:
|
|
241
|
+
while True:
|
|
242
|
+
pending = [
|
|
243
|
+
run
|
|
244
|
+
for run in runs
|
|
245
|
+
if run.recorded.get("status")
|
|
246
|
+
not in {
|
|
247
|
+
"succeeded",
|
|
248
|
+
"failed",
|
|
249
|
+
"cancelled",
|
|
250
|
+
}
|
|
251
|
+
]
|
|
252
|
+
if not pending or not wait:
|
|
253
|
+
break
|
|
254
|
+
time.sleep(30)
|
|
255
|
+
for run in pending:
|
|
256
|
+
recorded = cal._get_run(run.tenant, run.run_id)
|
|
257
|
+
if recorded is None:
|
|
258
|
+
raise UsageError(f"treatment_run_missing: {run.tenant.tenant_tag}")
|
|
259
|
+
run.recorded = recorded
|
|
260
|
+
for run in runs:
|
|
261
|
+
status = run.recorded.get("status")
|
|
262
|
+
if status != "succeeded":
|
|
263
|
+
raise UsageError(f"run_not_succeeded: {run.tenant.tenant_tag} ({status})")
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _identity(
|
|
267
|
+
runs: list[_Run],
|
|
268
|
+
k: int,
|
|
269
|
+
pins: Mapping[str, str],
|
|
270
|
+
) -> tuple[dict[str, object], str, str]:
|
|
271
|
+
runtimes: list[dict[str, object]] = []
|
|
272
|
+
digests: list[str] = []
|
|
273
|
+
suites: list[str] = []
|
|
274
|
+
for run in runs:
|
|
275
|
+
tag = run.tenant.tenant_tag
|
|
276
|
+
metadata = run.recorded.get("metadata")
|
|
277
|
+
if not isinstance(metadata, Mapping):
|
|
278
|
+
raise UsageError(f"run_metadata_missing: {tag}")
|
|
279
|
+
# Admission omits samples_per_task when it is 1.
|
|
280
|
+
samples = metadata.get("samples_per_task", 1)
|
|
281
|
+
if isinstance(samples, bool) or not isinstance(samples, int) or samples != k:
|
|
282
|
+
raise UsageError(f"k_mismatch: {tag}")
|
|
283
|
+
if metadata.get("platform_grader_criterion_versions", {}) != pins:
|
|
284
|
+
raise UsageError(f"criterion_pins_mismatch: {tag}")
|
|
285
|
+
runtime = metadata.get("platform_grader_runtime")
|
|
286
|
+
if not isinstance(runtime, dict) or not runtime:
|
|
287
|
+
raise UsageError(f"runtime_pin_missing: {tag}")
|
|
288
|
+
preprocessing = run.recorded.get("grader_preprocessing")
|
|
289
|
+
digest = (
|
|
290
|
+
preprocessing.get("grader_preprocessing_digest")
|
|
291
|
+
if isinstance(preprocessing, Mapping)
|
|
292
|
+
else None
|
|
293
|
+
)
|
|
294
|
+
if not isinstance(digest, str):
|
|
295
|
+
raise UsageError(f"preprocessing_digest_missing: {tag}")
|
|
296
|
+
suite = runner.suite_digest_from_run(run.recorded)
|
|
297
|
+
if suite is None:
|
|
298
|
+
raise UsageError(f"grader_suite_digest_missing: {tag}")
|
|
299
|
+
runtimes.append(runtime)
|
|
300
|
+
digests.append(runner.require_digest(digest, "recorded preprocessing digest"))
|
|
301
|
+
suites.append(suite)
|
|
302
|
+
tags = ", ".join(run.tenant.tenant_tag for run in runs)
|
|
303
|
+
for values, reason in (
|
|
304
|
+
(runtimes, "runtime_pin_mismatch"),
|
|
305
|
+
(digests, "preprocessing_digest_mismatch"),
|
|
306
|
+
(suites, "grader_suite_digest_mismatch"),
|
|
307
|
+
):
|
|
308
|
+
if any(value != values[0] for value in values[1:]):
|
|
309
|
+
raise UsageError(f"{reason}: tenants disagree ({tags})")
|
|
310
|
+
return runtimes[0], digests[0], suites[0]
|