envloop-cli 1.4.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 +103 -34
- envloop/cli_errors.py +153 -100
- envloop/cli_output.py +15 -16
- envloop/client.py +15 -33
- envloop/http.py +78 -25
- envloop/inference.py +35 -0
- envloop/job_spec_cli.py +138 -0
- envloop/resources.py +0 -4
- envloop/rsi_cli.py +16 -5
- envloop/terminal.py +77 -0
- envloop/train.py +26 -2
- envloop/train_log_cli.py +33 -29
- envloop/train_runner_cli.py +64 -0
- envloop/training/__init__.py +3 -122
- envloop_cli-1.10.0.dist-info/METADATA +218 -0
- envloop_cli-1.10.0.dist-info/RECORD +33 -0
- envloop/training/credentials.py +0 -107
- envloop/training/sampling_client.py +0 -58
- envloop/training/service_client.py +0 -165
- envloop_cli-1.4.0.dist-info/METADATA +0 -122
- envloop_cli-1.4.0.dist-info/RECORD +0 -32
- {envloop_cli-1.4.0.dist-info → envloop_cli-1.10.0.dist-info}/WHEEL +0 -0
- {envloop_cli-1.4.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,11 +147,11 @@ 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:
|
|
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;
|
|
140
|
-
submit.add_argument("--command", help="shell
|
|
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)")
|
|
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
|
|
|
143
157
|
def parser():
|
|
@@ -147,16 +161,26 @@ 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
|
-
job_submit = jobs.add_parser("submit", help="Submit an Agent, Sandbox, Train or Inference Job")
|
|
157
|
-
job_submit.add_argument("--type", choices=("agent", "sandbox", "train", "inference"), default=
|
|
158
|
-
job_submit.add_argument("--image", metavar="IMAGE", help="Sandbox
|
|
159
|
-
|
|
170
|
+
job_submit = jobs.add_parser("submit", help="Submit an Agent, Sandbox, Train or Inference Job", allow_abbrev=False)
|
|
171
|
+
job_submit.add_argument("--type", choices=("agent", "sandbox", "train", "inference"), default=None, help="Job type (default: spec type, otherwise agent)")
|
|
172
|
+
job_submit.add_argument("--image", metavar="IMAGE", help="Sandbox image override (server default: python:3.11); Train Runner: public spelling of --docker")
|
|
173
|
+
from .train_runner_cli import memory_mebibytes, positive_integer
|
|
174
|
+
|
|
175
|
+
job_submit.add_argument("--docker", metavar="IMAGE", help="Train Runner Docker image override; omitted uses the Runner declaration")
|
|
176
|
+
job_submit.add_argument("--trails", type=positive_integer, metavar="COUNT", help="Train Runner: 1–16 independent Trials (default: 1)")
|
|
177
|
+
job_submit.add_argument("--gput-type", metavar="MODEL", help="Train Runner: GPU model for each Trial (for example H100)")
|
|
178
|
+
job_submit.add_argument("--gpu", type=positive_integer, metavar="COUNT", help="Train Runner: positive GPU count per Trial")
|
|
179
|
+
job_submit.add_argument("--cpu", type=positive_integer, metavar="COUNT", help="Train Runner: positive CPU count per Trial")
|
|
180
|
+
for option in ('memory', 'disk', 'shm-size'):
|
|
181
|
+
job_submit.add_argument('--' + option, type=memory_mebibytes, metavar="SIZE",
|
|
182
|
+
help="Train Runner: per-Trial size in Mi/Gi/Ti (for example 64Gi)")
|
|
183
|
+
job_submit.add_argument("--spec", type=Path, metavar="REQUEST.json", help="Job specification JSON; explicit submission options override file values (except --trails)")
|
|
160
184
|
job_submit.add_argument("--source", type=Path, help="two-file directory for a single-task Job")
|
|
161
185
|
job_submit.add_argument("--task-id", help="select one task within the dataset")
|
|
162
186
|
job_submit.add_argument(
|
|
@@ -165,20 +189,21 @@ def parser():
|
|
|
165
189
|
)
|
|
166
190
|
job_submit.add_argument("--dataset", dest="dataset_option", type=dataset_reference, metavar="NAME[@VERSION]",
|
|
167
191
|
help="dataset name; default selection uses the platform's pinned snapshot")
|
|
168
|
-
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")
|
|
169
193
|
job_submit.add_argument("--split", help="one complete split name; omitted selects all splits")
|
|
170
194
|
job_submit.add_argument("--revision", help="Git branch/tag/commit; must match the platform's pinned snapshot")
|
|
171
195
|
runner_arguments(job_submit)
|
|
172
196
|
job_submit.add_argument("--attempts", type=int)
|
|
173
|
-
job_submit.add_argument("--request-id")
|
|
197
|
+
job_submit.add_argument("--request-id", help="idempotency key; overrides request_id in --spec")
|
|
174
198
|
job_submit.add_argument("--profile", type=profile_reference, metavar="NAME[@VERSION]",
|
|
175
|
-
help="named runtime Profile; omitted version
|
|
199
|
+
help="named runtime Profile; omitted version prefers default, then newest unarchived creation")
|
|
176
200
|
job_submit.add_argument("--seed", type=int)
|
|
177
201
|
job_submit.add_argument("--timeout-sec", type=wait_seconds,
|
|
178
|
-
help="Agent/Sandbox execution timeout (whole seconds); Train wait timeout (default: 600)")
|
|
202
|
+
help="Agent/Sandbox or Train Runner execution timeout (whole seconds); Train recipe --wait timeout (default: 600)")
|
|
179
203
|
job_submit.add_argument("--delete-delay", type=nonnegative_seconds, metavar="SECONDS",
|
|
180
204
|
help="keep each sandbox (E2B or Fly) after completion for SECONDS (default: 0); cancellation/timeout delete immediately")
|
|
181
205
|
job_submit.add_argument("--wait", action="store_true")
|
|
206
|
+
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)")
|
|
182
207
|
for action in ("get", "stop", "result", "pause", "resume", "wake", "event"):
|
|
183
208
|
command = jobs.add_parser(action)
|
|
184
209
|
command.add_argument("job_id")
|
|
@@ -206,7 +231,7 @@ def parser():
|
|
|
206
231
|
runs_list = runs.add_parser("list", help="List agent runs, most recently updated first")
|
|
207
232
|
runs_list.add_argument("--json", action="store_true", help="Output the complete response as JSON (default: table)")
|
|
208
233
|
runs_get = runs.add_parser("get", help="Show a run's rounds, Train Jobs and checkpoints")
|
|
209
|
-
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")
|
|
210
235
|
runs_get.add_argument("--json", action="store_true", help="Output the complete run as JSON (default: tables)")
|
|
211
236
|
runs_start = runs.add_parser("start", help="Start a new RSI loop run (paid; needs training.run)")
|
|
212
237
|
runs_start.add_argument("run", metavar="RUN", help="new run id: lower-case letters, digits and dashes")
|
|
@@ -222,6 +247,7 @@ def parser():
|
|
|
222
247
|
runs_start.add_argument("--student-model", help="gateway student model of a dry run")
|
|
223
248
|
runs_live = runs.add_parser("live", help="Show what a run does now; --follow streams new events")
|
|
224
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")
|
|
225
251
|
runs_live.add_argument("--since", type=int, help="only events after this cursor (the previous next_cursor)")
|
|
226
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")
|
|
227
253
|
runs_steer = runs.add_parser("steer", help="Message the running operator now (needs training.run)")
|
|
@@ -262,7 +288,7 @@ def parser():
|
|
|
262
288
|
update = profiles.add_parser("update", help="Edit an existing Profile by publishing a new immutable version")
|
|
263
289
|
update.add_argument("name")
|
|
264
290
|
update.add_argument("version", help="New version; existing versions cannot be overwritten")
|
|
265
|
-
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")
|
|
266
292
|
update.add_argument("--file", type=Path, required=True, help="TOML or JSON patch: merge params keys, replace supplied metadata; preserve omitted fields")
|
|
267
293
|
update.add_argument("--runner", metavar="RUNNER", help="Bind a Runner revision; v1 requires a full commit; omit to preserve the source binding")
|
|
268
294
|
update_secret = update.add_mutually_exclusive_group()
|
|
@@ -367,11 +393,24 @@ def resolve_cli_connection(api_url=None, workspace_id=None, project_id=None):
|
|
|
367
393
|
|
|
368
394
|
|
|
369
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):
|
|
370
403
|
cli = parser()
|
|
371
404
|
argv = sys.argv[1:] if argv is None else argv
|
|
372
405
|
if not argv:
|
|
373
406
|
cli.print_help()
|
|
374
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()
|
|
375
414
|
if any(value == "--api-key" or value.startswith("--api-key=") for value in argv):
|
|
376
415
|
cli.error("API keys must be entered with auth config or supplied through ENVLOOP_API_KEY")
|
|
377
416
|
args = cli.parse_args(argv)
|
|
@@ -386,30 +425,57 @@ def main(argv=None):
|
|
|
386
425
|
from .cli_errors import format_error
|
|
387
426
|
from .config import ConfigError
|
|
388
427
|
|
|
389
|
-
print(format_error(ConfigError('AUTH_INPUT_CANCELLED')
|
|
428
|
+
print(format_error(ConfigError('AUTH_INPUT_CANCELLED')), file=sys.stderr)
|
|
390
429
|
return 1
|
|
391
430
|
except Exception as exc:
|
|
392
431
|
from .cli_errors import format_error
|
|
393
432
|
|
|
394
|
-
print(format_error(exc
|
|
433
|
+
print(format_error(exc), file=sys.stderr)
|
|
395
434
|
return 1
|
|
396
435
|
if args.group == "job" and args.action == "submit":
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
436
|
+
from .train_runner_cli import SHORTCUT_FIELDS, runner_request
|
|
437
|
+
|
|
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")
|
|
400
460
|
if args.type == 'inference' and (args.wait or args.timeout_sec is not None):
|
|
401
461
|
cli.error('Inference is a continuous service; --wait and --timeout-sec do not apply')
|
|
402
462
|
agent_fields = ("dataset", "dataset_option", "source", "task_id", "config", "split", "revision",
|
|
403
|
-
"
|
|
404
|
-
|
|
463
|
+
"job_id", "tag", "priority", "attempts", "profile", "seed", "delete_delay")
|
|
464
|
+
if not runner_mode:
|
|
465
|
+
agent_fields += ('runner', 'command', 'request_id', 'image')
|
|
405
466
|
if any(getattr(args, key) is not None for key in agent_fields):
|
|
406
|
-
cli.error(f"--type {args.type}
|
|
467
|
+
cli.error(f"--type {args.type} cannot be combined with Agent submission options or mixed --spec/Runner input")
|
|
468
|
+
if runner_mode:
|
|
469
|
+
try:
|
|
470
|
+
args.train_request = runner_request(args)
|
|
471
|
+
except ValueError as exc:
|
|
472
|
+
cli.error(str(exc))
|
|
407
473
|
elif args.type == 'sandbox':
|
|
408
474
|
from .sandbox import sandbox_runner_selector, validate_image
|
|
409
475
|
|
|
410
476
|
if any(getattr(args, key) is not None for key in (
|
|
411
|
-
'
|
|
412
|
-
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')
|
|
413
479
|
try:
|
|
414
480
|
args.runner = sandbox_runner_selector(args.runner)
|
|
415
481
|
if args.image is not None:
|
|
@@ -421,8 +487,8 @@ def main(argv=None):
|
|
|
421
487
|
cli.error('Sandbox --timeout-sec must be whole seconds')
|
|
422
488
|
args.timeout_sec = int(args.timeout_sec)
|
|
423
489
|
else:
|
|
424
|
-
if args.
|
|
425
|
-
cli.error("--
|
|
490
|
+
if args.spec is not None:
|
|
491
|
+
cli.error("--spec requires --type train or inference")
|
|
426
492
|
if args.image is not None:
|
|
427
493
|
cli.error('--image requires --type sandbox')
|
|
428
494
|
if args.runner is not None:
|
|
@@ -504,16 +570,19 @@ def main(argv=None):
|
|
|
504
570
|
elif args.group == "job":
|
|
505
571
|
if args.action == "submit":
|
|
506
572
|
if args.type == 'inference':
|
|
507
|
-
result = client.jobs.submit_inference(
|
|
573
|
+
result = client.jobs.submit_inference(args.job_request)
|
|
508
574
|
elif args.type == "train":
|
|
509
|
-
|
|
575
|
+
request = args.train_request if args.spec is None else args.job_request
|
|
576
|
+
result = client.training_jobs.submit(request)
|
|
577
|
+
elif args.spec is not None:
|
|
578
|
+
result = client.jobs.submit_spec(args.job_request)
|
|
510
579
|
else:
|
|
511
580
|
result = client.jobs.submit(args.dataset, type=args.type, source=args.source, task_id=args.task_id,
|
|
512
581
|
attempts=args.attempts if args.attempts is not None else 1,
|
|
513
582
|
request_id=args.request_id, config=args.config, split=args.split,
|
|
514
583
|
revision=args.revision, **submission_options(args))
|
|
515
584
|
if args.wait:
|
|
516
|
-
wait_options = {"timeout": args.timeout_sec} if args.type == "train" and args.timeout_sec is not None else {}
|
|
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 {}
|
|
517
586
|
result = client.jobs.wait(result["job_id"], **wait_options)
|
|
518
587
|
elif args.action == 'event':
|
|
519
588
|
result = client.jobs.events(args.job_id)
|
|
@@ -583,10 +652,10 @@ def main(argv=None):
|
|
|
583
652
|
|
|
584
653
|
if isinstance(exc, CleanupError):
|
|
585
654
|
print(json.dumps(operation_output(exc.operation), ensure_ascii=False))
|
|
586
|
-
print(format_error(exc
|
|
655
|
+
print(format_error(exc), file=sys.stderr)
|
|
587
656
|
return 1
|
|
588
657
|
# Do not emit arbitrary exception messages, file content or credentials.
|
|
589
|
-
print(format_error(exc
|
|
658
|
+
print(format_error(exc), file=sys.stderr)
|
|
590
659
|
return 1
|
|
591
660
|
|
|
592
661
|
|