envloop-cli 1.7.0__py3-none-any.whl → 1.12.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.
- envloop/agent_runs.py +7 -3
- envloop/auth_cli.py +1 -1
- envloop/cli.py +87 -32
- envloop/cli_errors.py +152 -104
- envloop/cli_output.py +15 -16
- envloop/client.py +16 -3
- envloop/http.py +78 -25
- envloop/job_logs.py +6 -2
- envloop/job_spec_cli.py +138 -0
- envloop/rsi_cli.py +16 -5
- envloop/terminal.py +77 -0
- envloop/train.py +9 -4
- envloop/train_log_cli.py +60 -29
- envloop/train_progress.py +51 -0
- {envloop_cli-1.7.0.dist-info → envloop_cli-1.12.0.dist-info}/METADATA +26 -3
- envloop_cli-1.12.0.dist-info/RECORD +34 -0
- envloop_cli-1.7.0.dist-info/RECORD +0 -31
- {envloop_cli-1.7.0.dist-info → envloop_cli-1.12.0.dist-info}/WHEEL +0 -0
- {envloop_cli-1.7.0.dist-info → envloop_cli-1.12.0.dist-info}/entry_points.txt +0 -0
envloop/agent_runs.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Agent runs: a read-only view of Train Jobs grouped by their agent's run label.
|
|
2
2
|
|
|
3
3
|
The platform stores no run object. Agents tag every Train Job they create with
|
|
4
|
-
``
|
|
4
|
+
``tags`` (the RSI loop: ``rsi_run``, ``rsi_round``, ``rsi_checkpoint``...);
|
|
5
5
|
this module pages through ``GET /v1/job`` and derives runs from those tags,
|
|
6
6
|
using the same rules as the Console's agent runs page.
|
|
7
7
|
"""
|
|
@@ -34,7 +34,11 @@ class AgentRunError(RuntimeError):
|
|
|
34
34
|
|
|
35
35
|
|
|
36
36
|
def _metadata(job) -> dict:
|
|
37
|
-
metadata = job.get("
|
|
37
|
+
metadata = job.get("tags")
|
|
38
|
+
if not isinstance(metadata, dict):
|
|
39
|
+
metadata = job.get("labels")
|
|
40
|
+
if not isinstance(metadata, dict):
|
|
41
|
+
metadata = job.get("user_metadata")
|
|
38
42
|
return metadata if isinstance(metadata, dict) else {}
|
|
39
43
|
|
|
40
44
|
|
|
@@ -111,7 +115,7 @@ def _job_view(job) -> dict:
|
|
|
111
115
|
"lora_rank": job.get("lora_rank"),
|
|
112
116
|
"model_id": job.get("model_id"),
|
|
113
117
|
"checkpoint": _checkpoint(job),
|
|
114
|
-
"
|
|
118
|
+
"tags": metadata,
|
|
115
119
|
}
|
|
116
120
|
|
|
117
121
|
|
envloop/auth_cli.py
CHANGED
|
@@ -282,7 +282,7 @@ def run_auth(args, parser):
|
|
|
282
282
|
report["check"] = {401: "authentication_rejected", 403: "authorization_denied"}.get(
|
|
283
283
|
getattr(error, "status_code", None), safe_error_code(error))
|
|
284
284
|
_print(report, args.json)
|
|
285
|
-
print(format_error(error
|
|
285
|
+
print(format_error(error), file=sys.stderr)
|
|
286
286
|
return 1
|
|
287
287
|
_print(report, getattr(args, "json", False))
|
|
288
288
|
return 0
|
envloop/cli.py
CHANGED
|
@@ -39,6 +39,8 @@ class HelpArgumentParser(argparse.ArgumentParser):
|
|
|
39
39
|
def __init__(self, *args, **kwargs):
|
|
40
40
|
kwargs.setdefault('formatter_class', CommandHelpFormatter)
|
|
41
41
|
super().__init__(*args, **kwargs)
|
|
42
|
+
self.add_argument("--no-color", action="store_true", default=argparse.SUPPRESS,
|
|
43
|
+
help="Disable terminal highlighting")
|
|
42
44
|
self.set_defaults(_help_parser=self)
|
|
43
45
|
self._help_parser = self
|
|
44
46
|
|
|
@@ -61,9 +63,21 @@ class HelpArgumentParser(argparse.ArgumentParser):
|
|
|
61
63
|
def error(self, message):
|
|
62
64
|
if message.startswith("unrecognized arguments:"):
|
|
63
65
|
message = "unrecognized arguments"
|
|
66
|
+
# argparse includes raw argument values in these diagnostics. Keep the
|
|
67
|
+
# option/type description while excluding values (which can be secrets).
|
|
68
|
+
if "invalid choice:" in message:
|
|
69
|
+
message = message.split("invalid choice:", 1)[0] + "invalid choice"
|
|
70
|
+
elif " value:" in message:
|
|
71
|
+
message = message.split(" value:", 1)[0] + " value"
|
|
72
|
+
elif message.startswith("ambiguous option:"):
|
|
73
|
+
message = "ambiguous option"
|
|
74
|
+
from .cli_errors import error_block
|
|
75
|
+
|
|
64
76
|
command = self._help_parser
|
|
77
|
+
print(error_block("INVALID_ARGUMENT", message), file=sys.stderr)
|
|
78
|
+
print(file=sys.stderr)
|
|
65
79
|
command.print_help(sys.stderr)
|
|
66
|
-
command.exit(2
|
|
80
|
+
command.exit(2)
|
|
67
81
|
|
|
68
82
|
|
|
69
83
|
class CommandHelpFormatter(argparse.HelpFormatter):
|
|
@@ -133,10 +147,10 @@ def log_page_size(value):
|
|
|
133
147
|
|
|
134
148
|
def runner_arguments(submit):
|
|
135
149
|
submit.add_argument("--runner", metavar="NAME[@REVISION]",
|
|
136
|
-
help="Agent/Train: namespace/name@REVISION; Sandbox: registered Runner name (for example base)")
|
|
137
|
-
submit.add_argument("--job-id", help="caller-selected Job ID")
|
|
138
|
-
submit.add_argument("--tag", help="run label")
|
|
139
|
-
submit.add_argument("--priority", type=int, help="waiting priority;
|
|
150
|
+
help="Agent/Train/Inference: namespace/name@REVISION; Sandbox: registered Runner name (for example base)")
|
|
151
|
+
submit.add_argument("--job-id", help="Agent/Sandbox: caller-selected Job ID")
|
|
152
|
+
submit.add_argument("--tag", help="Agent/Sandbox: run label")
|
|
153
|
+
submit.add_argument("--priority", type=int, help="Agent/Sandbox/Train recipe: waiting priority; explicit value overrides spec (server default: 0)")
|
|
140
154
|
submit.add_argument("--command", help="Agent/Sandbox: shell override; Train: declared run_command parameter passed to the published Runner entrypoint; preserves multiline text")
|
|
141
155
|
|
|
142
156
|
|
|
@@ -147,14 +161,14 @@ def parser():
|
|
|
147
161
|
description="Manage Agent, Sandbox, Train and Inference Jobs and worker trials with EnvLoop.\nConfigure your connection with 'auth config'.",
|
|
148
162
|
)
|
|
149
163
|
cli._optionals.title = "Options"
|
|
150
|
-
cli.set_defaults(api_url=None, workspace_id=None)
|
|
164
|
+
cli.set_defaults(api_url=None, workspace_id=None, no_color=False)
|
|
151
165
|
cli.add_argument("-V", "--version", action="version", version=f"%(prog)s {CLI_VERSION}", help="Display CLI version")
|
|
152
166
|
groups = cli.add_subparsers(dest="group", title="Commands", prog=cli.prog, required=True)
|
|
153
167
|
from .auth_cli import add_auth_commands
|
|
154
168
|
|
|
155
169
|
jobs = groups.add_parser("job", help="Submit and manage Agent, Sandbox, Train and Inference Jobs").add_subparsers(dest="action", required=True)
|
|
156
170
|
job_submit = jobs.add_parser("submit", help="Submit an Agent, Sandbox, Train or Inference Job", allow_abbrev=False)
|
|
157
|
-
job_submit.add_argument("--type", choices=("agent", "sandbox", "train", "inference"), default=
|
|
171
|
+
job_submit.add_argument("--type", choices=("agent", "sandbox", "train", "inference"), default=None, help="Job type (default: spec type, otherwise agent)")
|
|
158
172
|
job_submit.add_argument("--image", metavar="IMAGE", help="Sandbox image override (server default: python:3.11); Train Runner: public spelling of --docker")
|
|
159
173
|
from .train_runner_cli import memory_mebibytes, positive_integer
|
|
160
174
|
|
|
@@ -166,7 +180,7 @@ def parser():
|
|
|
166
180
|
for option in ('memory', 'disk', 'shm-size'):
|
|
167
181
|
job_submit.add_argument('--' + option, type=memory_mebibytes, metavar="SIZE",
|
|
168
182
|
help="Train Runner: per-Trial size in Mi/Gi/Ti (for example 64Gi)")
|
|
169
|
-
job_submit.add_argument("--
|
|
183
|
+
job_submit.add_argument("--spec", type=Path, metavar="REQUEST.json", help="Job specification JSON; explicit submission options override file values (except --trails)")
|
|
170
184
|
job_submit.add_argument("--source", type=Path, help="two-file directory for a single-task Job")
|
|
171
185
|
job_submit.add_argument("--task-id", help="select one task within the dataset")
|
|
172
186
|
job_submit.add_argument(
|
|
@@ -175,17 +189,17 @@ def parser():
|
|
|
175
189
|
)
|
|
176
190
|
job_submit.add_argument("--dataset", dest="dataset_option", type=dataset_reference, metavar="NAME[@VERSION]",
|
|
177
191
|
help="dataset name; default selection uses the platform's pinned snapshot")
|
|
178
|
-
job_submit.add_argument("--config", help="dataset subset;
|
|
192
|
+
job_submit.add_argument("--config", help="Agent: dataset subset; Train recipe: named preset; omitted preserves spec or server default")
|
|
179
193
|
job_submit.add_argument("--split", help="one complete split name; omitted selects all splits")
|
|
180
194
|
job_submit.add_argument("--revision", help="Git branch/tag/commit; must match the platform's pinned snapshot")
|
|
181
195
|
runner_arguments(job_submit)
|
|
182
196
|
job_submit.add_argument("--attempts", type=int)
|
|
183
|
-
job_submit.add_argument("--request-id")
|
|
197
|
+
job_submit.add_argument("--request-id", help="idempotency key; overrides request_id in --spec")
|
|
184
198
|
job_submit.add_argument("--profile", type=profile_reference, metavar="NAME[@VERSION]",
|
|
185
|
-
help="named runtime Profile; omitted version
|
|
199
|
+
help="named runtime Profile; omitted version prefers default, then newest unarchived creation")
|
|
186
200
|
job_submit.add_argument("--seed", type=int)
|
|
187
201
|
job_submit.add_argument("--timeout-sec", type=wait_seconds,
|
|
188
|
-
help="Agent/Sandbox or Train Runner execution timeout (whole seconds); Train --
|
|
202
|
+
help="Agent/Sandbox or Train Runner execution timeout (whole seconds); Train recipe --wait timeout (default: 600)")
|
|
189
203
|
job_submit.add_argument("--delete-delay", type=nonnegative_seconds, metavar="SECONDS",
|
|
190
204
|
help="keep each sandbox (E2B or Fly) after completion for SECONDS (default: 0); cancellation/timeout delete immediately")
|
|
191
205
|
job_submit.add_argument("--wait", action="store_true")
|
|
@@ -207,6 +221,8 @@ def parser():
|
|
|
207
221
|
train_log = jobs.add_parser("log", help="Read or follow Agent, Sandbox or Train Job logs")
|
|
208
222
|
train_log.add_argument("job_id")
|
|
209
223
|
train_log.add_argument("--source", choices=("all", "system", "runtime"), default="all")
|
|
224
|
+
train_log.add_argument("--process", metavar="NAME",
|
|
225
|
+
help="Only one recorded process, e.g. trainer (a Train Job's step metrics)")
|
|
210
226
|
position = train_log.add_mutually_exclusive_group()
|
|
211
227
|
position.add_argument("--tail", type=log_page_size, help="Start with the last N records (default: 200)")
|
|
212
228
|
position.add_argument("--cursor", help="Resume from a previously returned log cursor")
|
|
@@ -217,7 +233,7 @@ def parser():
|
|
|
217
233
|
runs_list = runs.add_parser("list", help="List agent runs, most recently updated first")
|
|
218
234
|
runs_list.add_argument("--json", action="store_true", help="Output the complete response as JSON (default: table)")
|
|
219
235
|
runs_get = runs.add_parser("get", help="Show a run's rounds, Train Jobs and checkpoints")
|
|
220
|
-
runs_get.add_argument("run", metavar="RUN", help="run
|
|
236
|
+
runs_get.add_argument("run", metavar="RUN", help="run tag: tags.rsi_run or agent_run")
|
|
221
237
|
runs_get.add_argument("--json", action="store_true", help="Output the complete run as JSON (default: tables)")
|
|
222
238
|
runs_start = runs.add_parser("start", help="Start a new RSI loop run (paid; needs training.run)")
|
|
223
239
|
runs_start.add_argument("run", metavar="RUN", help="new run id: lower-case letters, digits and dashes")
|
|
@@ -233,6 +249,7 @@ def parser():
|
|
|
233
249
|
runs_start.add_argument("--student-model", help="gateway student model of a dry run")
|
|
234
250
|
runs_live = runs.add_parser("live", help="Show what a run does now; --follow streams new events")
|
|
235
251
|
runs_live.add_argument("run", metavar="RUN")
|
|
252
|
+
runs_live.add_argument("--json", action="store_true", help="Output JSON; --follow emits complete frames as NDJSON")
|
|
236
253
|
runs_live.add_argument("--since", type=int, help="only events after this cursor (the previous next_cursor)")
|
|
237
254
|
runs_live.add_argument("--follow", action="store_true", help="poll and print new events until the run ends; Ctrl-C exits without stopping it")
|
|
238
255
|
runs_steer = runs.add_parser("steer", help="Message the running operator now (needs training.run)")
|
|
@@ -273,7 +290,7 @@ def parser():
|
|
|
273
290
|
update = profiles.add_parser("update", help="Edit an existing Profile by publishing a new immutable version")
|
|
274
291
|
update.add_argument("name")
|
|
275
292
|
update.add_argument("version", help="New version; existing versions cannot be overwritten")
|
|
276
|
-
update.add_argument("--from-version", help="Source version; omit to use
|
|
293
|
+
update.add_argument("--from-version", help="Source version; omit to use default, then newest unarchived creation")
|
|
277
294
|
update.add_argument("--file", type=Path, required=True, help="TOML or JSON patch: merge params keys, replace supplied metadata; preserve omitted fields")
|
|
278
295
|
update.add_argument("--runner", metavar="RUNNER", help="Bind a Runner revision; v1 requires a full commit; omit to preserve the source binding")
|
|
279
296
|
update_secret = update.add_mutually_exclusive_group()
|
|
@@ -378,11 +395,24 @@ def resolve_cli_connection(api_url=None, workspace_id=None, project_id=None):
|
|
|
378
395
|
|
|
379
396
|
|
|
380
397
|
def main(argv=None):
|
|
398
|
+
from .terminal import output_options
|
|
399
|
+
|
|
400
|
+
with output_options():
|
|
401
|
+
return _main(argv)
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def _main(argv=None):
|
|
381
405
|
cli = parser()
|
|
382
406
|
argv = sys.argv[1:] if argv is None else argv
|
|
383
407
|
if not argv:
|
|
384
408
|
cli.print_help()
|
|
385
409
|
return 0
|
|
410
|
+
# Apply display options before validation, even if an earlier argument is invalid.
|
|
411
|
+
options = argv[:argv.index("--")] if "--" in argv else argv
|
|
412
|
+
if "--no-color" in options:
|
|
413
|
+
from .terminal import disable_color
|
|
414
|
+
|
|
415
|
+
disable_color()
|
|
386
416
|
if any(value == "--api-key" or value.startswith("--api-key=") for value in argv):
|
|
387
417
|
cli.error("API keys must be entered with auth config or supplied through ENVLOOP_API_KEY")
|
|
388
418
|
args = cli.parse_args(argv)
|
|
@@ -397,22 +427,38 @@ def main(argv=None):
|
|
|
397
427
|
from .cli_errors import format_error
|
|
398
428
|
from .config import ConfigError
|
|
399
429
|
|
|
400
|
-
print(format_error(ConfigError('AUTH_INPUT_CANCELLED')
|
|
430
|
+
print(format_error(ConfigError('AUTH_INPUT_CANCELLED')), file=sys.stderr)
|
|
401
431
|
return 1
|
|
402
432
|
except Exception as exc:
|
|
403
433
|
from .cli_errors import format_error
|
|
404
434
|
|
|
405
|
-
print(format_error(exc
|
|
435
|
+
print(format_error(exc), file=sys.stderr)
|
|
406
436
|
return 1
|
|
407
437
|
if args.group == "job" and args.action == "submit":
|
|
408
438
|
from .train_runner_cli import SHORTCUT_FIELDS, runner_request
|
|
409
439
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
440
|
+
if args.spec is not None:
|
|
441
|
+
from .job_spec_cli import SpecFileError, spec_request
|
|
442
|
+
|
|
443
|
+
try:
|
|
444
|
+
args.job_request = spec_request(args)
|
|
445
|
+
except (OSError, UnicodeError, json.JSONDecodeError, SpecFileError) as exc:
|
|
446
|
+
from .cli_errors import format_error
|
|
447
|
+
|
|
448
|
+
print(format_error(exc), file=sys.stderr)
|
|
449
|
+
return 1
|
|
450
|
+
except (ValueError, TypeError) as exc:
|
|
451
|
+
cli.error(str(exc) if isinstance(exc, ValueError) else 'Unable to merge Job spec')
|
|
452
|
+
else:
|
|
453
|
+
args.type = args.type or 'agent'
|
|
454
|
+
runner_mode = args.type == 'train' and args.spec is None
|
|
455
|
+
if args.spec is None and not runner_mode and any(getattr(args, key) is not None for key in SHORTCUT_FIELDS):
|
|
456
|
+
cli.error('Train Runner shortcuts require --type train --runner and cannot be combined with --spec')
|
|
457
|
+
if args.spec is not None:
|
|
458
|
+
pass # The merged request was validated above; the platform owns final admission.
|
|
459
|
+
elif args.type in {"train", "inference"}:
|
|
460
|
+
if args.spec is None and not runner_mode:
|
|
461
|
+
cli.error(f"--type {args.type} requires --spec REQUEST.json")
|
|
416
462
|
if args.type == 'inference' and (args.wait or args.timeout_sec is not None):
|
|
417
463
|
cli.error('Inference is a continuous service; --wait and --timeout-sec do not apply')
|
|
418
464
|
agent_fields = ("dataset", "dataset_option", "source", "task_id", "config", "split", "revision",
|
|
@@ -420,7 +466,7 @@ def main(argv=None):
|
|
|
420
466
|
if not runner_mode:
|
|
421
467
|
agent_fields += ('runner', 'command', 'request_id', 'image')
|
|
422
468
|
if any(getattr(args, key) is not None for key in agent_fields):
|
|
423
|
-
cli.error(f"--type {args.type} cannot be combined with Agent submission options or mixed --
|
|
469
|
+
cli.error(f"--type {args.type} cannot be combined with Agent submission options or mixed --spec/Runner input")
|
|
424
470
|
if runner_mode:
|
|
425
471
|
try:
|
|
426
472
|
args.train_request = runner_request(args)
|
|
@@ -430,8 +476,8 @@ def main(argv=None):
|
|
|
430
476
|
from .sandbox import sandbox_runner_selector, validate_image
|
|
431
477
|
|
|
432
478
|
if any(getattr(args, key) is not None for key in (
|
|
433
|
-
'
|
|
434
|
-
cli.error('--type sandbox accepts --runner without Dataset, Task,
|
|
479
|
+
'spec', 'dataset', 'dataset_option', 'source', 'task_id', 'config', 'split', 'revision', 'seed')):
|
|
480
|
+
cli.error('--type sandbox accepts --runner without Dataset, Task, Job spec or seed options')
|
|
435
481
|
try:
|
|
436
482
|
args.runner = sandbox_runner_selector(args.runner)
|
|
437
483
|
if args.image is not None:
|
|
@@ -443,8 +489,8 @@ def main(argv=None):
|
|
|
443
489
|
cli.error('Sandbox --timeout-sec must be whole seconds')
|
|
444
490
|
args.timeout_sec = int(args.timeout_sec)
|
|
445
491
|
else:
|
|
446
|
-
if args.
|
|
447
|
-
cli.error("--
|
|
492
|
+
if args.spec is not None:
|
|
493
|
+
cli.error("--spec requires --type train or inference")
|
|
448
494
|
if args.image is not None:
|
|
449
495
|
cli.error('--image requires --type sandbox')
|
|
450
496
|
if args.runner is not None:
|
|
@@ -526,17 +572,19 @@ def main(argv=None):
|
|
|
526
572
|
elif args.group == "job":
|
|
527
573
|
if args.action == "submit":
|
|
528
574
|
if args.type == 'inference':
|
|
529
|
-
result = client.jobs.submit_inference(
|
|
575
|
+
result = client.jobs.submit_inference(args.job_request)
|
|
530
576
|
elif args.type == "train":
|
|
531
|
-
request = args.train_request if args.
|
|
577
|
+
request = args.train_request if args.spec is None else args.job_request
|
|
532
578
|
result = client.training_jobs.submit(request)
|
|
579
|
+
elif args.spec is not None:
|
|
580
|
+
result = client.jobs.submit_spec(args.job_request)
|
|
533
581
|
else:
|
|
534
582
|
result = client.jobs.submit(args.dataset, type=args.type, source=args.source, task_id=args.task_id,
|
|
535
583
|
attempts=args.attempts if args.attempts is not None else 1,
|
|
536
584
|
request_id=args.request_id, config=args.config, split=args.split,
|
|
537
585
|
revision=args.revision, **submission_options(args))
|
|
538
586
|
if args.wait:
|
|
539
|
-
wait_options = {"timeout": args.timeout_sec} if args.type == "train" and args.
|
|
587
|
+
wait_options = {"timeout": args.timeout_sec} if args.type == "train" and args.spec is not None and args.timeout_sec is not None else {}
|
|
540
588
|
result = client.jobs.wait(result["job_id"], **wait_options)
|
|
541
589
|
elif args.action == 'event':
|
|
542
590
|
result = client.jobs.events(args.job_id)
|
|
@@ -563,6 +611,13 @@ def main(argv=None):
|
|
|
563
611
|
result = client.jobs.list(**options)
|
|
564
612
|
else:
|
|
565
613
|
result = getattr(client.jobs, 'status' if args.action == 'get' else args.action)(args.job_id)
|
|
614
|
+
if args.action == 'get':
|
|
615
|
+
from .train_progress import job_progress, progress_line
|
|
616
|
+
|
|
617
|
+
# The JSON on stdout is unchanged; a Train Job's live progress is a readable stderr line.
|
|
618
|
+
line = progress_line(job_progress(result))
|
|
619
|
+
if line:
|
|
620
|
+
print(line, file=sys.stderr)
|
|
566
621
|
elif args.group == "trail":
|
|
567
622
|
if args.action == "artifact":
|
|
568
623
|
if args.artifact_action == "download":
|
|
@@ -606,10 +661,10 @@ def main(argv=None):
|
|
|
606
661
|
|
|
607
662
|
if isinstance(exc, CleanupError):
|
|
608
663
|
print(json.dumps(operation_output(exc.operation), ensure_ascii=False))
|
|
609
|
-
print(format_error(exc
|
|
664
|
+
print(format_error(exc), file=sys.stderr)
|
|
610
665
|
return 1
|
|
611
666
|
# Do not emit arbitrary exception messages, file content or credentials.
|
|
612
|
-
print(format_error(exc
|
|
667
|
+
print(format_error(exc), file=sys.stderr)
|
|
613
668
|
return 1
|
|
614
669
|
|
|
615
670
|
|