envloop-cli 1.7.0__py3-none-any.whl → 1.10.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 +78 -32
- envloop/cli_errors.py +152 -104
- envloop/cli_output.py +15 -16
- envloop/client.py +9 -0
- envloop/http.py +78 -25
- envloop/job_spec_cli.py +138 -0
- envloop/rsi_cli.py +16 -5
- envloop/terminal.py +77 -0
- envloop/train.py +2 -1
- envloop/train_log_cli.py +33 -29
- {envloop_cli-1.7.0.dist-info → envloop_cli-1.10.0.dist-info}/METADATA +26 -3
- {envloop_cli-1.7.0.dist-info → envloop_cli-1.10.0.dist-info}/RECORD +16 -14
- {envloop_cli-1.7.0.dist-info → envloop_cli-1.10.0.dist-info}/WHEEL +0 -0
- {envloop_cli-1.7.0.dist-info → envloop_cli-1.10.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")
|
|
@@ -217,7 +231,7 @@ def parser():
|
|
|
217
231
|
runs_list = runs.add_parser("list", help="List agent runs, most recently updated first")
|
|
218
232
|
runs_list.add_argument("--json", action="store_true", help="Output the complete response as JSON (default: table)")
|
|
219
233
|
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
|
|
234
|
+
runs_get.add_argument("run", metavar="RUN", help="run tag: tags.rsi_run or agent_run")
|
|
221
235
|
runs_get.add_argument("--json", action="store_true", help="Output the complete run as JSON (default: tables)")
|
|
222
236
|
runs_start = runs.add_parser("start", help="Start a new RSI loop run (paid; needs training.run)")
|
|
223
237
|
runs_start.add_argument("run", metavar="RUN", help="new run id: lower-case letters, digits and dashes")
|
|
@@ -233,6 +247,7 @@ def parser():
|
|
|
233
247
|
runs_start.add_argument("--student-model", help="gateway student model of a dry run")
|
|
234
248
|
runs_live = runs.add_parser("live", help="Show what a run does now; --follow streams new events")
|
|
235
249
|
runs_live.add_argument("run", metavar="RUN")
|
|
250
|
+
runs_live.add_argument("--json", action="store_true", help="Output JSON; --follow emits complete frames as NDJSON")
|
|
236
251
|
runs_live.add_argument("--since", type=int, help="only events after this cursor (the previous next_cursor)")
|
|
237
252
|
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
253
|
runs_steer = runs.add_parser("steer", help="Message the running operator now (needs training.run)")
|
|
@@ -273,7 +288,7 @@ def parser():
|
|
|
273
288
|
update = profiles.add_parser("update", help="Edit an existing Profile by publishing a new immutable version")
|
|
274
289
|
update.add_argument("name")
|
|
275
290
|
update.add_argument("version", help="New version; existing versions cannot be overwritten")
|
|
276
|
-
update.add_argument("--from-version", help="Source version; omit to use
|
|
291
|
+
update.add_argument("--from-version", help="Source version; omit to use default, then newest unarchived creation")
|
|
277
292
|
update.add_argument("--file", type=Path, required=True, help="TOML or JSON patch: merge params keys, replace supplied metadata; preserve omitted fields")
|
|
278
293
|
update.add_argument("--runner", metavar="RUNNER", help="Bind a Runner revision; v1 requires a full commit; omit to preserve the source binding")
|
|
279
294
|
update_secret = update.add_mutually_exclusive_group()
|
|
@@ -378,11 +393,24 @@ def resolve_cli_connection(api_url=None, workspace_id=None, project_id=None):
|
|
|
378
393
|
|
|
379
394
|
|
|
380
395
|
def main(argv=None):
|
|
396
|
+
from .terminal import output_options
|
|
397
|
+
|
|
398
|
+
with output_options():
|
|
399
|
+
return _main(argv)
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def _main(argv=None):
|
|
381
403
|
cli = parser()
|
|
382
404
|
argv = sys.argv[1:] if argv is None else argv
|
|
383
405
|
if not argv:
|
|
384
406
|
cli.print_help()
|
|
385
407
|
return 0
|
|
408
|
+
# Apply display options before validation, even if an earlier argument is invalid.
|
|
409
|
+
options = argv[:argv.index("--")] if "--" in argv else argv
|
|
410
|
+
if "--no-color" in options:
|
|
411
|
+
from .terminal import disable_color
|
|
412
|
+
|
|
413
|
+
disable_color()
|
|
386
414
|
if any(value == "--api-key" or value.startswith("--api-key=") for value in argv):
|
|
387
415
|
cli.error("API keys must be entered with auth config or supplied through ENVLOOP_API_KEY")
|
|
388
416
|
args = cli.parse_args(argv)
|
|
@@ -397,22 +425,38 @@ def main(argv=None):
|
|
|
397
425
|
from .cli_errors import format_error
|
|
398
426
|
from .config import ConfigError
|
|
399
427
|
|
|
400
|
-
print(format_error(ConfigError('AUTH_INPUT_CANCELLED')
|
|
428
|
+
print(format_error(ConfigError('AUTH_INPUT_CANCELLED')), file=sys.stderr)
|
|
401
429
|
return 1
|
|
402
430
|
except Exception as exc:
|
|
403
431
|
from .cli_errors import format_error
|
|
404
432
|
|
|
405
|
-
print(format_error(exc
|
|
433
|
+
print(format_error(exc), file=sys.stderr)
|
|
406
434
|
return 1
|
|
407
435
|
if args.group == "job" and args.action == "submit":
|
|
408
436
|
from .train_runner_cli import SHORTCUT_FIELDS, runner_request
|
|
409
437
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
438
|
+
if args.spec is not None:
|
|
439
|
+
from .job_spec_cli import SpecFileError, spec_request
|
|
440
|
+
|
|
441
|
+
try:
|
|
442
|
+
args.job_request = spec_request(args)
|
|
443
|
+
except (OSError, UnicodeError, json.JSONDecodeError, SpecFileError) as exc:
|
|
444
|
+
from .cli_errors import format_error
|
|
445
|
+
|
|
446
|
+
print(format_error(exc), file=sys.stderr)
|
|
447
|
+
return 1
|
|
448
|
+
except (ValueError, TypeError) as exc:
|
|
449
|
+
cli.error(str(exc) if isinstance(exc, ValueError) else 'Unable to merge Job spec')
|
|
450
|
+
else:
|
|
451
|
+
args.type = args.type or 'agent'
|
|
452
|
+
runner_mode = args.type == 'train' and args.spec is None
|
|
453
|
+
if args.spec is None and not runner_mode and any(getattr(args, key) is not None for key in SHORTCUT_FIELDS):
|
|
454
|
+
cli.error('Train Runner shortcuts require --type train --runner and cannot be combined with --spec')
|
|
455
|
+
if args.spec is not None:
|
|
456
|
+
pass # The merged request was validated above; the platform owns final admission.
|
|
457
|
+
elif args.type in {"train", "inference"}:
|
|
458
|
+
if args.spec is None and not runner_mode:
|
|
459
|
+
cli.error(f"--type {args.type} requires --spec REQUEST.json")
|
|
416
460
|
if args.type == 'inference' and (args.wait or args.timeout_sec is not None):
|
|
417
461
|
cli.error('Inference is a continuous service; --wait and --timeout-sec do not apply')
|
|
418
462
|
agent_fields = ("dataset", "dataset_option", "source", "task_id", "config", "split", "revision",
|
|
@@ -420,7 +464,7 @@ def main(argv=None):
|
|
|
420
464
|
if not runner_mode:
|
|
421
465
|
agent_fields += ('runner', 'command', 'request_id', 'image')
|
|
422
466
|
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 --
|
|
467
|
+
cli.error(f"--type {args.type} cannot be combined with Agent submission options or mixed --spec/Runner input")
|
|
424
468
|
if runner_mode:
|
|
425
469
|
try:
|
|
426
470
|
args.train_request = runner_request(args)
|
|
@@ -430,8 +474,8 @@ def main(argv=None):
|
|
|
430
474
|
from .sandbox import sandbox_runner_selector, validate_image
|
|
431
475
|
|
|
432
476
|
if any(getattr(args, key) is not None for key in (
|
|
433
|
-
'
|
|
434
|
-
cli.error('--type sandbox accepts --runner without Dataset, Task,
|
|
477
|
+
'spec', 'dataset', 'dataset_option', 'source', 'task_id', 'config', 'split', 'revision', 'seed')):
|
|
478
|
+
cli.error('--type sandbox accepts --runner without Dataset, Task, Job spec or seed options')
|
|
435
479
|
try:
|
|
436
480
|
args.runner = sandbox_runner_selector(args.runner)
|
|
437
481
|
if args.image is not None:
|
|
@@ -443,8 +487,8 @@ def main(argv=None):
|
|
|
443
487
|
cli.error('Sandbox --timeout-sec must be whole seconds')
|
|
444
488
|
args.timeout_sec = int(args.timeout_sec)
|
|
445
489
|
else:
|
|
446
|
-
if args.
|
|
447
|
-
cli.error("--
|
|
490
|
+
if args.spec is not None:
|
|
491
|
+
cli.error("--spec requires --type train or inference")
|
|
448
492
|
if args.image is not None:
|
|
449
493
|
cli.error('--image requires --type sandbox')
|
|
450
494
|
if args.runner is not None:
|
|
@@ -526,17 +570,19 @@ def main(argv=None):
|
|
|
526
570
|
elif args.group == "job":
|
|
527
571
|
if args.action == "submit":
|
|
528
572
|
if args.type == 'inference':
|
|
529
|
-
result = client.jobs.submit_inference(
|
|
573
|
+
result = client.jobs.submit_inference(args.job_request)
|
|
530
574
|
elif args.type == "train":
|
|
531
|
-
request = args.train_request if args.
|
|
575
|
+
request = args.train_request if args.spec is None else args.job_request
|
|
532
576
|
result = client.training_jobs.submit(request)
|
|
577
|
+
elif args.spec is not None:
|
|
578
|
+
result = client.jobs.submit_spec(args.job_request)
|
|
533
579
|
else:
|
|
534
580
|
result = client.jobs.submit(args.dataset, type=args.type, source=args.source, task_id=args.task_id,
|
|
535
581
|
attempts=args.attempts if args.attempts is not None else 1,
|
|
536
582
|
request_id=args.request_id, config=args.config, split=args.split,
|
|
537
583
|
revision=args.revision, **submission_options(args))
|
|
538
584
|
if args.wait:
|
|
539
|
-
wait_options = {"timeout": args.timeout_sec} if args.type == "train" and args.
|
|
585
|
+
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
586
|
result = client.jobs.wait(result["job_id"], **wait_options)
|
|
541
587
|
elif args.action == 'event':
|
|
542
588
|
result = client.jobs.events(args.job_id)
|
|
@@ -606,10 +652,10 @@ def main(argv=None):
|
|
|
606
652
|
|
|
607
653
|
if isinstance(exc, CleanupError):
|
|
608
654
|
print(json.dumps(operation_output(exc.operation), ensure_ascii=False))
|
|
609
|
-
print(format_error(exc
|
|
655
|
+
print(format_error(exc), file=sys.stderr)
|
|
610
656
|
return 1
|
|
611
657
|
# Do not emit arbitrary exception messages, file content or credentials.
|
|
612
|
-
print(format_error(exc
|
|
658
|
+
print(format_error(exc), file=sys.stderr)
|
|
613
659
|
return 1
|
|
614
660
|
|
|
615
661
|
|