envloop-cli-experiment 1.23.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,11 @@
1
+ Metadata-Version: 2.5
2
+ Name: envloop-cli-experiment
3
+ Version: 1.23.0
4
+ Summary: EnvLoop experiment client functionality installed with the base client
5
+ Requires-Python: >=3.12
6
+ Requires-Dist: envloop-cli==1.23.0
7
+ Description-Content-Type: text/markdown
8
+
9
+ # envloop-cli-experiment
10
+
11
+ Experiment implementation installed by default with envloop-cli 1.23.0. This project is a source candidate, not a claim that this version has been published. Install with its matching base wheel. Only the base owns the el/envloop scripts.
@@ -0,0 +1,23 @@
1
+ envloop_experiment/__init__.py,sha256=nKDGmzFNAQv2mq16YijiI3Q-gDL_smNyZD-AQaqoz7Y,68
2
+ envloop_experiment/cli.py,sha256=salxVeXhoVhaK4m2nk-5f4g9KxV12q-A3eaLkCvpctU,28393
3
+ envloop_experiment/cli_output.py,sha256=QAKQqH9ZTRNLwNbG6K0KLOpnQ_jbsUZWZcYlAdx_KYY,2689
4
+ envloop_experiment/client.py,sha256=cYtL1uM3Cc6yTOi9Rwfi79mv1ontrbyyay-o_4w98KU,25094
5
+ envloop_experiment/decision.py,sha256=apsds-9tXmn-xN8IU3IU-Ye9E2P32jpN49DcVgc_JnM,917
6
+ envloop_experiment/http.py,sha256=Mjhp6KojeFEUGBSMIDw8SFKE4v_h6_gEseBZs4S61iM,5600
7
+ envloop_experiment/inference.py,sha256=74MZDs0lqqqNwgdvj58rs-D3emDj4iHv1W-copc5PYA,2134
8
+ envloop_experiment/job_cost.py,sha256=-h4tYQo3wm6W4ZRRa-_HeGsaBzRoDqRi5mo6_iNrfD4,1910
9
+ envloop_experiment/job_logs.py,sha256=RLipbj89e5hQdBeXtszsxSYYw7By1hscwSTGFQzQ3Qw,1340
10
+ envloop_experiment/job_spec_cli.py,sha256=brVAFud0D0TQ_e80t1kSswztG-AE02eZI3iO-Uxi5sA,7909
11
+ envloop_experiment/navigation_resources.py,sha256=xoI4VFmd0dzviKvzndwKAoWdfbaL_f50cnXRxPkfoo4,1832
12
+ envloop_experiment/resource_cli.py,sha256=YLa5-mOxxx0uonDN-dY5oE_fvrhN7g9ea4NAg6LY8kg,1604
13
+ envloop_experiment/resources.py,sha256=Ss_dW2J4lDRB-3aVnOnUwQgUAYOK49_GvF5NO2_Zlhs,7167
14
+ envloop_experiment/runtime_params.py,sha256=ITF6hDuhfdChnWo3e8NW27WxEoNoCuOTww-Hz60zqQU,3608
15
+ envloop_experiment/sandbox.py,sha256=sTpAWbcs2jAPfb7gILK4MTJSmcpSeZG3anFkLA5TgdQ,1274
16
+ envloop_experiment/train.py,sha256=yNrSguzd7phjhRQsrHo0fQ1gruzmjKqAKrs2HHai_Ig,4940
17
+ envloop_experiment/train_log_cli.py,sha256=fsmt9I6Hyntnj_rdqEe4YBnJsAPYsfFAsW-xRCSC6nM,5838
18
+ envloop_experiment/train_progress.py,sha256=Cln8NPWfmSH9Mnlml-78dOXqwZTqWVeJ8ArDWFAQv-A,2536
19
+ envloop_experiment/train_runner_cli.py,sha256=v8a1ibI5ymf994_F6kHCiidNaxibRAZMF9tqDY4HBWA,3135
20
+ envloop_experiment/training/__init__.py,sha256=IEQWbCHe-zZ2e7JQxOrFOdYlSQxMfOceCZUf_KeszBk,221
21
+ envloop_cli_experiment-1.23.0.dist-info/METADATA,sha256=eCslEmA5tmCFqrQtliCWoqLqXiwi8lnuwQeLLwJcyYE,512
22
+ envloop_cli_experiment-1.23.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
23
+ envloop_cli_experiment-1.23.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1 @@
1
+ """Optional EnvLoop client functionality; loaded only on demand."""
@@ -0,0 +1,467 @@
1
+ import argparse
2
+ import json
3
+ import math
4
+ import sys
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+
9
+ def dataset_reference(value: str):
10
+ name, separator, version = value.rpartition("@")
11
+ if (not value.strip() or (separator and (not name.strip() or not version.strip()))):
12
+ raise argparse.ArgumentTypeError(
13
+ "expected name or name@version with a non-empty name and version "
14
+ "(e.g. org/dataset@1.0)"
15
+ )
16
+ return value
17
+
18
+
19
+ def profile_reference(value: str):
20
+ name, separator, version = value.partition("@")
21
+ if (not name.strip() or name != name.strip() or "/" not in name
22
+ or (separator and (not version.strip() or version != version.strip()))):
23
+ raise argparse.ArgumentTypeError("expected a Profile name org/name or org/name@version")
24
+ return value
25
+
26
+
27
+ def nonnegative_seconds(value: str):
28
+ try:
29
+ seconds = int(value)
30
+ except ValueError:
31
+ raise argparse.ArgumentTypeError("expected a non-negative integer in seconds") from None
32
+ if seconds < 0:
33
+ raise argparse.ArgumentTypeError("expected a non-negative integer in seconds")
34
+ return seconds
35
+
36
+
37
+ def max_cost_usd(value: str):
38
+ from envloop_experiment.client import job_budget
39
+ try:
40
+ return job_budget(float(value))['max_cost_usd']
41
+ except ValueError:
42
+ raise argparse.ArgumentTypeError("expected a USD amount from 0.000001 to 100000") from None
43
+
44
+
45
+ def wait_seconds(value: str):
46
+ try:
47
+ seconds = float(value)
48
+ except ValueError:
49
+ raise argparse.ArgumentTypeError("seconds must be finite and non-negative") from None
50
+ if not math.isfinite(seconds) or seconds < 0:
51
+ raise argparse.ArgumentTypeError("seconds must be finite and non-negative")
52
+ return seconds
53
+
54
+
55
+ def log_page_size(value: str):
56
+ try:
57
+ count = int(value)
58
+ except ValueError:
59
+ raise argparse.ArgumentTypeError("expected an integer from 1 to 1000") from None
60
+ if not 1 <= count <= 1000:
61
+ raise argparse.ArgumentTypeError("expected an integer from 1 to 1000")
62
+ return count
63
+
64
+
65
+ def runner_arguments(submit: argparse.ArgumentParser):
66
+ submit.add_argument("--runner", metavar="NAME[@REVISION]",
67
+ help="Agent/Train/Inference: namespace/name@REVISION; Sandbox: registered Runner name (for example base)")
68
+ submit.add_argument("--job-id", help="Agent/Sandbox: caller-selected Job ID")
69
+ submit.add_argument("--tag", help="Agent/Sandbox: run label")
70
+ submit.add_argument("--priority", type=int, help="Agent/Sandbox/Train recipe: waiting priority; explicit value overrides spec (server default: 0)")
71
+ submit.add_argument("--command", help="Agent/Sandbox: shell override; Train: declared run_command parameter passed to the published Runner entrypoint; preserves multiline text")
72
+
73
+
74
+ def submission_options(args: argparse.Namespace):
75
+ options = {}
76
+ for key in ("runner", "job_id", "tag", "command", "image"):
77
+ value = getattr(args, key, None)
78
+ if value is not None:
79
+ options[key] = value
80
+ options["priority"] = args.priority if args.priority is not None else 0
81
+ if args.params is not None:
82
+ options["params"] = args.params
83
+ if args.max_cost_usd is not None:
84
+ options["max_cost_usd"] = args.max_cost_usd
85
+ if args.profile:
86
+ if args.runner is not None:
87
+ options["profile_id"] = args.profile
88
+ else:
89
+ name, separator, version = args.profile.partition("@")
90
+ options["profile_name"] = name
91
+ if separator:
92
+ options["profile_version"] = version
93
+ if args.seed is not None:
94
+ if args.runner is not None:
95
+ options["profile"] = {"seed": args.seed}
96
+ else:
97
+ options["seed"] = args.seed
98
+ execution = {key: value for key, value in {"timeout_sec": args.timeout_sec,
99
+ "delete_delay_sec": args.delete_delay}.items() if value is not None}
100
+ if execution:
101
+ options["execution"] = execution
102
+ return options
103
+
104
+
105
+ def add_commands(groups: "argparse._SubParsersAction[argparse.ArgumentParser]"):
106
+ jobs = groups.add_parser("job", help="Submit and manage Agent, Sandbox, Train and Inference Jobs").add_subparsers(dest="action", required=True)
107
+ job_submit = jobs.add_parser("submit", help="Submit an Agent, Sandbox, Train or Inference Job", allow_abbrev=False)
108
+ job_submit.add_argument("--type", choices=("agent", "sandbox", "train", "inference"), default=None, help="Job type (default: spec type, otherwise agent)")
109
+ job_submit.add_argument("--image", metavar="IMAGE", help="Sandbox image override (server default: python:3.11); Train Runner: public spelling of --docker")
110
+ from envloop_experiment.train_runner_cli import memory_mebibytes, positive_integer
111
+
112
+ job_submit.add_argument("--docker", metavar="IMAGE", help="Train Runner Docker image override; omitted uses the Runner declaration")
113
+ job_submit.add_argument("--trails", type=positive_integer, metavar="COUNT", help="Train Runner: 1–16 independent Trials (default: 1)")
114
+ job_submit.add_argument("--gput-type", metavar="MODEL", help="Train Runner: GPU model for each Trial (for example H100)")
115
+ job_submit.add_argument("--gpu", type=positive_integer, metavar="COUNT", help="Train Runner: positive GPU count per Trial")
116
+ job_submit.add_argument("--cpu", type=positive_integer, metavar="COUNT", help="Train Runner: positive CPU count per Trial")
117
+ for option in ('memory', 'disk', 'shm-size'):
118
+ job_submit.add_argument('--' + option, type=memory_mebibytes, metavar="SIZE",
119
+ help="Train Runner: per-Trial size in Mi/Gi/Ti (for example 64Gi)")
120
+ job_submit.add_argument("--spec", type=Path, metavar="REQUEST.json", help="Job specification JSON; explicit submission options override file values (except --trails)")
121
+ job_submit.add_argument("--source", type=Path, help="two-file directory for a single-task Job")
122
+ job_submit.add_argument("--task-id", help="select one task within the dataset")
123
+ job_submit.add_argument(
124
+ "dataset", nargs="?", type=dataset_reference, metavar="NAME[@VERSION]",
125
+ help="dataset name; optional author version (legacy positional form)",
126
+ )
127
+ job_submit.add_argument("--dataset", dest="dataset_option", type=dataset_reference, metavar="NAME[@VERSION]",
128
+ help="dataset name; default selection uses the platform's pinned snapshot")
129
+ job_submit.add_argument("--config", help="Agent: dataset subset; Train recipe: named preset; omitted preserves spec or server default")
130
+ job_submit.add_argument("--split", help="one complete split name; omitted selects all splits")
131
+ job_submit.add_argument("--revision", help="Git branch/tag/commit; must match the platform's pinned snapshot")
132
+ runner_arguments(job_submit)
133
+ job_submit.add_argument("--attempts", type=int)
134
+ job_submit.add_argument("--request-id", help="idempotency key; overrides request_id in --spec")
135
+ job_submit.add_argument("--profile", type=profile_reference, metavar="NAME[@VERSION]",
136
+ help="named runtime Profile; omitted version prefers default, then newest unarchived creation")
137
+ from envloop_experiment.runtime_params import ParamsAction, parse_params
138
+ job_submit.add_argument("--params", type=parse_params, action=ParamsAction, metavar="JSON|KEY=VALUE",
139
+ help="Repeatable highest-priority Agent/Sandbox runtime parameter patch; JSON preserves types, KV values are strings; later keys win")
140
+ job_submit.add_argument("--seed", type=int)
141
+ job_submit.add_argument("--timeout-sec", type=wait_seconds,
142
+ help="Agent/Sandbox or Train Runner execution timeout (whole seconds); Train recipe --wait timeout (default: 600)")
143
+ job_submit.add_argument("--max-cost-usd", type=max_cost_usd, metavar="USD",
144
+ help="Agent/Sandbox: Job spend cap in USD (model calls plus sandbox time); the Job stops "
145
+ "with error BUDGET_EXCEEDED once it is reached")
146
+ job_submit.add_argument("--delete-delay", type=nonnegative_seconds, metavar="SECONDS",
147
+ help="keep each sandbox (E2B or Fly) after completion for SECONDS (default: 0); cancellation/timeout delete immediately")
148
+ job_submit.add_argument("--wait", action="store_true")
149
+ job_submit.add_argument("--json", action="store_true", help="Output the complete response as JSON (the default for submit; accepted for consistency with list commands)")
150
+ for action in ("get", "stop", "result", "pause", "resume", "wake", "event"):
151
+ command = jobs.add_parser(action)
152
+ command.add_argument("job_id")
153
+ if action == 'event':
154
+ command.add_argument('--json', action='store_true')
155
+ command.set_defaults(_list_view=True)
156
+ regrade = jobs.add_parser("regrade", help="Re-run only the verifier of sealed compose Trials, optionally with a reward set",
157
+ allow_abbrev=False)
158
+ regrade_source = regrade.add_mutually_exclusive_group(required=True)
159
+ regrade_source.add_argument("--job", metavar="JOB_ID", help="regrade every sealed Trial of this Agent Job")
160
+ regrade_source.add_argument("--trial", action="append", metavar="TRIAL_ID",
161
+ help="regrade this Trial (repeatable; every one must be eligible)")
162
+ regrade.add_argument("--reward-set", metavar="URI", help="envloop://reward-sets/NS/NAME@VERSION; omitted uses each Task's own tests")
163
+ regrade.add_argument("--reward-set-sha256", metavar="HEX", help="manifest SHA-256 pinned with --reward-set (required with it)")
164
+ regrade_runner = regrade.add_mutually_exclusive_group()
165
+ regrade_runner.add_argument("--runner", metavar="NAME@REVISION",
166
+ help="Runner Revision with regrade support (revision, branch or tag); needed for Trials "
167
+ "sealed by a Revision without it")
168
+ regrade_runner.add_argument("--runner-ref", type=Path, metavar="RUNNER_REF.json",
169
+ help="the same as --runner, as a five-field Revision pin file")
170
+ regrade.add_argument("--max-cost-usd", type=float, metavar="USD", help="Job spend cap in USD (needs platform Job budgets)")
171
+ regrade.add_argument("--request-id", help="idempotency key (default: derived from the request, so a retry returns the "
172
+ "same Job; give a new one to run the same regrade again)")
173
+ regrade.add_argument("--wait", action="store_true", help="poll until the regrade Job ends")
174
+ regrade.add_argument("--timeout-sec", type=wait_seconds, help="--wait timeout in whole seconds (default: 600)")
175
+ regrade.add_argument("--json", action="store_true", help="Output the complete response as JSON (the default)")
176
+ decision = jobs.add_parser("decision", help="Invoke the frozen decision plugin of an Inference Job")
177
+ decision.add_argument("job_id")
178
+ decision.add_argument("--model", required=True)
179
+ decision.add_argument("--spec", type=Path, required=True, metavar="PAYLOAD.json")
180
+ from envloop_experiment.resource_cli import add_job_resources
181
+ add_job_resources(jobs)
182
+ listing = jobs.add_parser("list")
183
+ listing.add_argument('--type', choices=['agent', 'sandbox', 'regrade', 'train', 'inference'],
184
+ help='Select Agent, Sandbox, Regrade, Train or Inference Jobs')
185
+ listing.add_argument('--status', help='Filter by Job status')
186
+ listing.add_argument("--cursor")
187
+ listing.add_argument("--json", action="store_true", help="Output the complete response as JSON (default: table)")
188
+ listing.add_argument("--limit", type=int, default=25)
189
+ train_log = jobs.add_parser("log", help="Read or follow Agent, Sandbox or Train Job logs")
190
+ train_log.add_argument("job_id")
191
+ train_log.add_argument("--source", choices=("all", "system", "runtime"), default="all")
192
+ train_log.add_argument("--process", metavar="NAME",
193
+ help="Only one recorded process, e.g. trainer (a Train Job's step metrics)")
194
+ position = train_log.add_mutually_exclusive_group()
195
+ position.add_argument("--tail", type=log_page_size, help="Start with the last N records (default: 200)")
196
+ position.add_argument("--cursor", help="Resume from a previously returned log cursor")
197
+ train_log.add_argument("--limit", type=log_page_size, default=200, help="Maximum records per page (default: 200)")
198
+ train_log.add_argument("--follow", action="store_true", help="Drain pages and poll until collection ends; Ctrl-C exits without stopping the Job")
199
+ train_log.add_argument("--json", action="store_true", help="Output the complete page as JSON; --follow emits one page per line (NDJSON)")
200
+ from envloop._optional import available
201
+ if available('rsi-loop'):
202
+ from envloop_rsi_loop.cli import add_commands as add_runs
203
+ add_runs(jobs)
204
+ tasks = groups.add_parser("trail", help="Inspect and control existing trials").add_subparsers(dest="action", required=True)
205
+ listing = tasks.add_parser("list", help="List Agent, Sandbox, Train and Inference worker Trials")
206
+ listing.add_argument("--type", choices=("agent", "sandbox", "train", "inference"), help="Filter by Trial type")
207
+ listing.add_argument("--job-id", help="Filter by parent Job ID")
208
+ listing.add_argument("--cursor")
209
+ listing.add_argument("--limit", type=int, default=25)
210
+ snapshot = tasks.add_parser("snapshot")
211
+ snapshot.add_argument("trial_id")
212
+ for action in ("get", "stop", "result"):
213
+ tasks.add_parser(action).add_argument("trial_id")
214
+ listing.add_argument("--json", action="store_true", help="Output the complete response as JSON")
215
+ artifacts = tasks.add_parser("artifact", help="List or download a trial's artifacts").add_subparsers(dest="artifact_action", required=True)
216
+ artifact_list = artifacts.add_parser("list", help="List one page of artifacts")
217
+ artifact_list.set_defaults(_list_view=True)
218
+ artifact_list.add_argument("trial_id")
219
+ artifact_list.add_argument("--cursor")
220
+ artifact_list.add_argument("--limit", type=int, default=25)
221
+ artifact_list.add_argument("--json", action="store_true")
222
+ download = artifacts.add_parser("download", help="Download all uploaded artifacts and verify their sizes and hashes")
223
+ download.add_argument("trial_id")
224
+ download.add_argument("directory", type=Path)
225
+
226
+ def validate(args: argparse.Namespace, cli: argparse.ArgumentParser):
227
+ if args.group == "job" and args.action == "decision":
228
+ from envloop_experiment.decision import DecisionClient
229
+ try:
230
+ if args.spec.stat().st_size > 1024 * 1024:
231
+ raise ValueError("Decision payload exceeds 1 MiB")
232
+ args.payload = json.loads(args.spec.read_text(encoding="utf-8"))
233
+ DecisionClient.validate(args.job_id, args.model, args.payload)
234
+ except (OSError, ValueError):
235
+ cli.error("decision requires a readable JSON object of at most 1 MiB, without model")
236
+ if args.group == "job" and args.action == "regrade":
237
+ if (args.reward_set is None) != (args.reward_set_sha256 is None):
238
+ cli.error('--reward-set and --reward-set-sha256 must be given together')
239
+ args.reward_set_pin = ({'uri': args.reward_set, 'manifest_sha256': args.reward_set_sha256}
240
+ if args.reward_set is not None else None)
241
+ import math
242
+ if args.max_cost_usd is not None and (not math.isfinite(args.max_cost_usd) or not 0.000001 <= args.max_cost_usd <= 100000):
243
+ cli.error('--max-cost-usd must be between 0.000001 and 100000')
244
+ if args.runner is not None and '@' not in args.runner:
245
+ cli.error('--runner must be NAME@REVISION, branch or tag')
246
+ args.runner_pin = None
247
+ if args.runner_ref is not None:
248
+ try:
249
+ args.runner_pin = json.loads(args.runner_ref.read_text(encoding='utf-8'))
250
+ except (OSError, UnicodeError, ValueError):
251
+ cli.error('--runner-ref must be a readable JSON Runner Revision pin')
252
+ if args.group == "job" and args.action == "submit":
253
+ from envloop_experiment.train_runner_cli import SHORTCUT_FIELDS, runner_request
254
+
255
+ if args.spec is not None:
256
+ from envloop_experiment.job_spec_cli import SpecFileError, spec_request
257
+
258
+ try:
259
+ args.job_request = spec_request(args)
260
+ except (OSError, UnicodeError, json.JSONDecodeError, SpecFileError) as exc:
261
+ from envloop.cli_errors import format_error
262
+
263
+ print(format_error(exc), file=sys.stderr)
264
+ return 1
265
+ except (ValueError, TypeError) as exc:
266
+ cli.error(str(exc) if isinstance(exc, ValueError) else 'Unable to merge Job spec')
267
+ else:
268
+ args.type = args.type or 'agent'
269
+ if args.params is not None and args.type not in {'agent', 'sandbox'}:
270
+ cli.error('--params applies to Agent/Sandbox declared Runner runtime parameters')
271
+ runner_mode = args.type == 'train' and args.spec is None
272
+ if args.spec is None and not runner_mode and any(getattr(args, key) is not None for key in SHORTCUT_FIELDS):
273
+ cli.error('Train Runner shortcuts require --type train --runner and cannot be combined with --spec')
274
+ if args.spec is not None:
275
+ pass # The merged request was validated above; the platform owns final admission.
276
+ elif args.type in {"train", "inference"}:
277
+ if args.spec is None and not runner_mode:
278
+ cli.error(f"--type {args.type} requires --spec REQUEST.json")
279
+ if args.type == 'inference' and (args.wait or args.timeout_sec is not None):
280
+ cli.error('Inference is a continuous service; --wait and --timeout-sec do not apply')
281
+ agent_fields = ("dataset", "dataset_option", "source", "task_id", "config", "split", "revision",
282
+ "job_id", "tag", "priority", "attempts", "profile", "seed", "delete_delay", "max_cost_usd")
283
+ if not runner_mode:
284
+ agent_fields += ('runner', 'command', 'request_id', 'image')
285
+ if any(getattr(args, key) is not None for key in agent_fields):
286
+ cli.error(f"--type {args.type} cannot be combined with Agent submission options or mixed --spec/Runner input")
287
+ if runner_mode:
288
+ try:
289
+ args.train_request = runner_request(args)
290
+ except ValueError as exc:
291
+ cli.error(str(exc))
292
+ elif args.type == 'sandbox':
293
+ from envloop_experiment.sandbox import (
294
+ sandbox_runner_selector,
295
+ validate_image,
296
+ )
297
+
298
+ if any(getattr(args, key) is not None for key in (
299
+ 'spec', 'dataset', 'dataset_option', 'source', 'task_id', 'config', 'split', 'revision', 'seed')):
300
+ cli.error('--type sandbox accepts --runner without Dataset, Task, Job spec or seed options')
301
+ try:
302
+ args.runner = sandbox_runner_selector(args.runner)
303
+ if args.image is not None:
304
+ args.image = validate_image(args.image)
305
+ except ValueError as exc:
306
+ cli.error(str(exc))
307
+ if args.timeout_sec is not None:
308
+ if not args.timeout_sec.is_integer():
309
+ cli.error('Sandbox --timeout-sec must be whole seconds')
310
+ args.timeout_sec = int(args.timeout_sec)
311
+ else:
312
+ if args.spec is not None:
313
+ cli.error("--spec requires --type train or inference")
314
+ if args.image is not None:
315
+ cli.error('--image requires --type sandbox')
316
+ if args.runner is not None:
317
+ from envloop_experiment.client import runner_selector
318
+
319
+ try:
320
+ args.runner = runner_selector(args.runner)
321
+ except ValueError as exc:
322
+ cli.error(str(exc))
323
+ selections = sum(value is not None for value in (args.dataset, args.dataset_option, args.source))
324
+ if selections != 1 and not (selections == 0 and args.profile):
325
+ cli.error("provide exactly one positional dataset, --dataset or --source")
326
+ args.dataset = args.dataset if args.dataset is not None else args.dataset_option
327
+ if args.source is not None and any(getattr(args, key) is not None for key in ("runner", "task_id", "config", "split", "revision")):
328
+ cli.error("dataset selection options require a dataset")
329
+ if args.timeout_sec is not None:
330
+ if not args.timeout_sec.is_integer():
331
+ cli.error("Agent --timeout-sec must be whole seconds")
332
+ args.timeout_sec = int(args.timeout_sec)
333
+ return None
334
+
335
+ def execute_job(args: argparse.Namespace, client: Any, cli: argparse.ArgumentParser):
336
+ from envloop._optional import CommandExit
337
+ if args.action == "submit":
338
+ if args.type == 'inference':
339
+ result = client.jobs.submit_inference(args.job_request)
340
+ elif args.type == "train":
341
+ request = args.train_request if args.spec is None else args.job_request
342
+ result = client.training_jobs.submit(request)
343
+ elif args.spec is not None:
344
+ result = client.jobs.submit_spec(args.job_request)
345
+ else:
346
+ result = client.jobs.submit(args.dataset, type=args.type, source=args.source, task_id=args.task_id,
347
+ attempts=args.attempts if args.attempts is not None else 1,
348
+ request_id=args.request_id, config=args.config, split=args.split,
349
+ revision=args.revision, **submission_options(args))
350
+ if args.wait:
351
+ wait_options = {"timeout": args.timeout_sec} if args.type == "train" and args.spec is not None and args.timeout_sec is not None else {}
352
+ result = client.jobs.wait(result["job_id"], **wait_options)
353
+ from envloop_experiment.job_cost import cost_line
354
+
355
+ line = cost_line(result)
356
+ if line:
357
+ print(line, file=sys.stderr)
358
+ if isinstance(result, dict) and result.get('status') == 'cancelled':
359
+ # A waited Job that did not complete (stopped, or its budget was reached) is not a
360
+ # success, like a failed one; the JSON on stdout says why (error, cost).
361
+ print(json.dumps(result, ensure_ascii=False))
362
+ raise CommandExit(1)
363
+ elif args.action == "regrade":
364
+ from envloop.http import RemoteError
365
+ try:
366
+ result = client.jobs.regrade(job_id=args.job, trial_ids=args.trial, reward_set=args.reward_set_pin,
367
+ runner_ref=args.runner_pin, runner=args.runner,
368
+ budget={'max_cost_usd': args.max_cost_usd}
369
+ if args.max_cost_usd is not None else None,
370
+ request_id=args.request_id)
371
+ except RemoteError as error:
372
+ hint = None
373
+ if error.code == 'JOB_REQUEST_CONFLICT' and args.request_id is None:
374
+ # The default request_id is derived from this command's options. A conflict means the
375
+ # same options resolved to another request, e.g. --runner NAME@BRANCH moved.
376
+ hint = ('The default request_id is derived from these options and was already used for a '
377
+ 'different request (for example --runner @branch now names another revision). '
378
+ 'Pass --request-id NEW to submit it as a new regrade, or pin --runner NAME@REVISION.')
379
+ elif error.code == 'REGRADE_RUNNER_UNSUPPORTED':
380
+ hint = ('The source Trials ran on a Runner Revision without regrade support. Pass '
381
+ '--runner envhub/harbor-compose@REVISION (a Revision whose runner.toml declares '
382
+ 'capabilities = ["regrade"]; see el runner list) or --runner-ref FILE.json.')
383
+ if hint is None:
384
+ raise
385
+ raise RemoteError(error.code, error.status_code, retry_after=error.retry_after,
386
+ message=error.message, hint=hint, request_id=error.request_id,
387
+ details=error.details) from None
388
+ if args.wait:
389
+ result = client.jobs.wait(result["job_id"], **({"timeout": args.timeout_sec}
390
+ if args.timeout_sec is not None else {}))
391
+ from envloop_experiment.job_cost import cost_line
392
+
393
+ line = cost_line(result)
394
+ if line:
395
+ print(line, file=sys.stderr)
396
+ if isinstance(result, dict) and result.get('status') == 'cancelled':
397
+ # A waited Job that did not complete (stopped, or its budget was reached) is not a
398
+ # success, like a failed one; the JSON on stdout says why (error, cost).
399
+ print(json.dumps(result, ensure_ascii=False))
400
+ raise CommandExit(1)
401
+ elif args.action == "decision":
402
+ result = client.decision.evaluate(args.job_id, model=args.model, payload=args.payload)
403
+ elif args.action == 'event':
404
+ result = client.jobs.events(args.job_id)
405
+ elif args.action in {'serving-profile', 'adapter'}:
406
+ from envloop_experiment.resource_cli import run_job_resource
407
+ result = run_job_resource(client, args)
408
+ elif args.action == "log":
409
+ from envloop_experiment.train_log_cli import read_logs
410
+
411
+ raise CommandExit(read_logs(client.jobs, args))
412
+ elif args.action == "run" and args.run_action in {"start", "live", "steer", "stop"}:
413
+ from envloop_rsi_loop.rsi_cli import run_rsi
414
+
415
+ result = run_rsi(client.agent_runs, args)
416
+ if result is None:
417
+ raise CommandExit(0)
418
+ elif args.action == "run":
419
+ result = client.agent_runs.list() if args.run_action == "list" else client.agent_runs.get(args.run)
420
+ elif args.action == "list":
421
+ options = {'cursor': args.cursor, 'limit': args.limit}
422
+ for field in ('type', 'status'):
423
+ if getattr(args, field) is not None:
424
+ options[field] = getattr(args, field)
425
+ result = client.jobs.list(**options)
426
+ else:
427
+ result = getattr(client.jobs, 'status' if args.action == 'get' else args.action)(args.job_id)
428
+ if args.action == 'get':
429
+ from envloop_experiment.train_progress import job_progress, progress_line
430
+
431
+ # The JSON on stdout is unchanged; a Train Job's live progress is a readable stderr line.
432
+ line = progress_line(job_progress(result))
433
+ if line:
434
+ print(line, file=sys.stderr)
435
+ if args.action in {'get', 'result'}:
436
+ from envloop_experiment.job_cost import cost_line
437
+
438
+ # An Agent/Sandbox Job's spend and budget as a readable stderr line; stdout stays JSON.
439
+ line = cost_line(result)
440
+ if line:
441
+ print(line, file=sys.stderr)
442
+ from envloop.cli_errors import failure_lines
443
+
444
+ for line in failure_lines(result):
445
+ print(line, file=sys.stderr)
446
+ return result
447
+
448
+ def execute_trail(args: argparse.Namespace, client: Any, cli: argparse.ArgumentParser):
449
+ if args.action == "artifact":
450
+ if args.artifact_action == "download":
451
+ result = client.trials.download_artifacts(args.trial_id, args.directory)
452
+ else:
453
+ result = client.trials.artifacts(args.trial_id, cursor=args.cursor, limit=args.limit)
454
+ elif args.action == "list":
455
+ options = {'cursor': args.cursor, 'limit': args.limit}
456
+ for field in ('type', 'job_id'):
457
+ if getattr(args, field) is not None:
458
+ options[field] = getattr(args, field)
459
+ result = client.trials.list(**options)
460
+ else:
461
+ result = getattr(client.trials, 'status' if args.action == 'get' else args.action)(args.trial_id)
462
+ if args.action in {'get', 'result'}:
463
+ from envloop.cli_errors import failure_lines
464
+
465
+ for line in failure_lines(result):
466
+ print(line, file=sys.stderr)
467
+ return result
@@ -0,0 +1,50 @@
1
+ import argparse
2
+ from collections.abc import Callable
3
+ from typing import Any
4
+
5
+ from envloop.cli_output import generic_layout, timestamp
6
+
7
+
8
+ def job_row(item: dict[str, Any]):
9
+ ref = item.get("dataset_ref") or {}
10
+ dataset = ref.get("name")
11
+ if dataset is not None and ref.get("version") is not None:
12
+ dataset = f"{dataset}@{ref['version']}"
13
+ status = item.get("status")
14
+ known = {"pending", "running", "stopping", "completed", "failed", "cancelled"}
15
+ training = item.get("training") or {}
16
+ if item.get("type") == "inference":
17
+ dataset = (item.get("inference") or {}).get("model")
18
+ if item.get("type") == "train":
19
+ dataset = training.get("base_model") or (training.get("spec") or {}).get("base_model")
20
+ return (
21
+ item.get("job_id"), item.get("type", "agent"), dataset if dataset is not None else item.get("name"),
22
+ status.capitalize() if status in known else status,
23
+ None if item.get("type") in {"train", "sandbox", "inference"} else
24
+ item.get("total_tasks") if "total_tasks" in item else
25
+ len(item["members"]) if isinstance(item.get("members"), list) else None,
26
+ timestamp(item.get("created_at")), timestamp(item.get("finished_at")),
27
+ )
28
+
29
+
30
+ def list_layout(items: Any, args: argparse.Namespace):
31
+ if args.group == 'job' and args.action == 'event':
32
+ headers = ('RESOURCE', 'STATUS', 'AT', 'SOURCE')
33
+ render: Callable[[dict[str, Any]], tuple[Any, ...]] = lambda item: (item.get('resource_id'), item.get('status'), timestamp(item.get('at')), item.get('source'))
34
+ empty = 'No events recorded.'
35
+ elif args.group == "job" and args.action not in {"adapter", "serving-profile", "event"}:
36
+ headers = ("JOB ID", "TYPE", "DATASET / MODEL", "STATUS", "TASKS", "CREATED AT", "ENDED AT")
37
+ render = job_row
38
+ empty = "No jobs found."
39
+ else:
40
+ return generic_layout(items, args, {'trail': ('artifact_id', 'name', 'state', 'size') if args.action == 'artifact' else ('trial_id', 'type', 'job_id', 'name', 'status', 'created_at'), 'job': ('adapter_id', 'name', 'status', 'model', 'serving_profile')})
41
+ return headers, render, empty
42
+
43
+ def pagination(command: list[str], args: argparse.Namespace, cursor: object):
44
+ if args.group == 'trail' and getattr(args, 'trial_id', None) is not None:
45
+ command.append(str(args.trial_id))
46
+ command.extend(['--limit', str(args.limit), '--cursor=' + str(cursor)])
47
+ fields = {'job': ('type', 'status'), 'trail': ('type', 'job_id')}
48
+ for field in fields.get(args.group, ()):
49
+ if getattr(args, field, None) is not None:
50
+ command.extend(['--' + field.replace('_', '-'), str(getattr(args, field))])