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.

Files changed (83) hide show
  1. convergent_cli/__init__.py +1 -0
  2. convergent_cli/auth.py +227 -0
  3. convergent_cli/build_context.py +225 -0
  4. convergent_cli/calibration_finish.py +310 -0
  5. convergent_cli/calibration_frontier.py +499 -0
  6. convergent_cli/calibration_runner.py +1413 -0
  7. convergent_cli/calibration_send.py +44 -0
  8. convergent_cli/codex_app_server.py +127 -0
  9. convergent_cli/command_tree.py +102 -0
  10. convergent_cli/commands/__init__.py +58 -0
  11. convergent_cli/commands/agents.py +729 -0
  12. convergent_cli/commands/auth.py +117 -0
  13. convergent_cli/commands/calibration.py +3857 -0
  14. convergent_cli/commands/calibration_labels_load.py +627 -0
  15. convergent_cli/commands/capabilities.py +85 -0
  16. convergent_cli/commands/dataset_authoring.py +964 -0
  17. convergent_cli/commands/dataset_import.py +763 -0
  18. convergent_cli/commands/datasets.py +678 -0
  19. convergent_cli/commands/docs.py +120 -0
  20. convergent_cli/commands/doctor.py +124 -0
  21. convergent_cli/commands/environments.py +63 -0
  22. convergent_cli/commands/findings.py +206 -0
  23. convergent_cli/commands/graders.py +803 -0
  24. convergent_cli/commands/harness.py +366 -0
  25. convergent_cli/commands/issues.py +388 -0
  26. convergent_cli/commands/production.py +173 -0
  27. convergent_cli/commands/profile.py +86 -0
  28. convergent_cli/commands/rubrics.py +14 -0
  29. convergent_cli/commands/runs.py +1763 -0
  30. convergent_cli/commands/setup.py +968 -0
  31. convergent_cli/commands/task_qc.py +1041 -0
  32. convergent_cli/commands/task_qc_reports.py +393 -0
  33. convergent_cli/commands/verdicts.py +156 -0
  34. convergent_cli/commands/workspaces.py +170 -0
  35. convergent_cli/commands/worlds.py +292 -0
  36. convergent_cli/confirm.py +59 -0
  37. convergent_cli/control_client.py +141 -0
  38. convergent_cli/customer_client.py +1272 -0
  39. convergent_cli/docs_client.py +165 -0
  40. convergent_cli/errors.py +212 -0
  41. convergent_cli/harness_run.py +1169 -0
  42. convergent_cli/harness_tree.py +854 -0
  43. convergent_cli/installed_agent.py +1071 -0
  44. convergent_cli/json_files.py +23 -0
  45. convergent_cli/local_files.py +64 -0
  46. convergent_cli/main.py +106 -0
  47. convergent_cli/oauth.py +431 -0
  48. convergent_cli/output.py +15 -0
  49. convergent_cli/paging.py +62 -0
  50. convergent_cli/run_calibration.py +361 -0
  51. convergent_cli/run_usage.py +99 -0
  52. convergent_cli/sessions.py +275 -0
  53. convergent_cli/setup_agents.py +190 -0
  54. convergent_cli/setup_auth.py +463 -0
  55. convergent_cli/setup_instrument.py +244 -0
  56. convergent_cli/setup_mcp.py +525 -0
  57. convergent_cli/setup_wizard.py +125 -0
  58. convergent_cli/skill_files/convergent/SKILL.md +25 -0
  59. convergent_cli/skill_files/convergent-instrument/SKILL.md +15 -0
  60. convergent_cli/skill_files/convergent-verify/SKILL.md +15 -0
  61. convergent_cli/skills.py +489 -0
  62. convergent_cli/state.py +297 -0
  63. convergent_cli/transport.py +242 -0
  64. convergent_cli/verdict_feedback.py +161 -0
  65. convergent_cli/verifier_bench_preflight.py +32 -0
  66. convergent_cli/world_docker.py +145 -0
  67. convergent_cli/world_files.py +432 -0
  68. convergent_cli/world_live.py +660 -0
  69. convergent_cli/world_mcp.py +109 -0
  70. convergent_cli/world_probe_requirements.txt +29 -0
  71. convergent_cli/world_template/Dockerfile +10 -0
  72. convergent_cli/world_template/README.md +38 -0
  73. convergent_cli/world_template/convergent.toml +11 -0
  74. convergent_cli/world_template/examples/default/environment/catalog.json +3 -0
  75. convergent_cli/world_template/examples/default/instruction.md +1 -0
  76. convergent_cli/world_template/requirements.txt +1 -0
  77. convergent_cli/world_template/schemas/catalog.json +5 -0
  78. convergent_cli/world_template/server.py +25 -0
  79. convergent_cli-0.1.0.dist-info/METADATA +679 -0
  80. convergent_cli-0.1.0.dist-info/RECORD +83 -0
  81. convergent_cli-0.1.0.dist-info/WHEEL +4 -0
  82. convergent_cli-0.1.0.dist-info/entry_points.txt +2 -0
  83. 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]