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 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,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; larger values run first (default: 0)")
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="agent", help="Job type (default: agent)")
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("--file", type=Path, help="Managed Train or Inference Job JSON (spec or Train trials array; interactive training is unsupported)")
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; 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")
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 requires an explicit default")
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 --file wait timeout (default: 600)")
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 label: user_metadata.rsi_run or agent_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 the explicit default")
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'), args=args, program=cli.prog), file=sys.stderr)
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, args=args, program=cli.prog), file=sys.stderr)
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
- runner_mode = args.type == 'train' and args.file is None
411
- if not runner_mode and any(getattr(args, key) is not None for key in SHORTCUT_FIELDS):
412
- cli.error('Train Runner shortcuts require --type train --runner and cannot be combined with --file')
413
- if args.type in {"train", "inference"}:
414
- if args.file is None and not runner_mode:
415
- cli.error(f"--type {args.type} requires --file REQUEST.json")
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 --file/Runner input")
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
- 'file', 'dataset', 'dataset_option', 'source', 'task_id', 'config', 'split', 'revision', 'seed')):
434
- cli.error('--type sandbox accepts --runner without Dataset, Task, Train file or seed options')
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.file is not None:
447
- cli.error("--file requires --type train or inference")
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(json.loads(args.file.read_text(encoding='utf-8')))
575
+ result = client.jobs.submit_inference(args.job_request)
530
576
  elif args.type == "train":
531
- request = args.train_request if args.file is None else json.loads(args.file.read_text(encoding="utf-8"))
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.file is not None and args.timeout_sec is not None else {}
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, args=args, program=cli.prog), file=sys.stderr)
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, args=args, program=cli.prog), file=sys.stderr)
667
+ print(format_error(exc), file=sys.stderr)
613
668
  return 1
614
669
 
615
670