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 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
- ``user_metadata`` (the RSI loop: ``rsi_run``, ``rsi_round``, ``rsi_checkpoint``...);
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("user_metadata")
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
- "user_metadata": metadata,
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, args=args, program=parser.prog), file=sys.stderr)
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, f"\n{command.prog}: error: {message}\n")
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: override the Dataset runner with 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; larger values run first (default: 0)")
140
- submit.add_argument("--command", help="shell script override executed in the runner directory; preserves multiline text")
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="agent", help="Job type (default: agent)")
158
- job_submit.add_argument("--image", metavar="IMAGE", help="Sandbox Docker image override (server default: python:3.11)")
159
- job_submit.add_argument("--file", type=Path, help="Train or Inference Job request JSON (spec or Train trials array)")
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; uses its declared default when omitted")
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 requires an explicit default")
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 label: user_metadata.rsi_run or agent_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 the explicit default")
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'), args=args, program=cli.prog), file=sys.stderr)
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, args=args, program=cli.prog), file=sys.stderr)
433
+ print(format_error(exc), file=sys.stderr)
395
434
  return 1
396
435
  if args.group == "job" and args.action == "submit":
397
- if args.type in {"train", "inference"}:
398
- if args.file is None:
399
- cli.error(f"--type {args.type} requires --file REQUEST.json")
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
- "runner", "job_id", "tag", "priority", "command", "attempts", "request_id",
404
- "profile", "seed", "delete_delay", "image")
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} accepts its request through --file; Agent submission options cannot be combined")
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
- 'file', 'dataset', 'dataset_option', 'source', 'task_id', 'config', 'split', 'revision', 'seed')):
412
- cli.error('--type sandbox accepts --runner without Dataset, Task, Train file or seed options')
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.file is not None:
425
- cli.error("--file requires --type train or inference")
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(json.loads(args.file.read_text(encoding='utf-8')))
573
+ result = client.jobs.submit_inference(args.job_request)
508
574
  elif args.type == "train":
509
- result = client.training_jobs.submit(json.loads(args.file.read_text(encoding="utf-8")))
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, args=args, program=cli.prog), file=sys.stderr)
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, args=args, program=cli.prog), file=sys.stderr)
658
+ print(format_error(exc), file=sys.stderr)
590
659
  return 1
591
660
 
592
661