kiteml-cli 0.6.0__tar.gz → 0.7.0__tar.gz

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.
Files changed (23) hide show
  1. {kiteml_cli-0.6.0 → kiteml_cli-0.7.0}/PKG-INFO +4 -2
  2. {kiteml_cli-0.6.0 → kiteml_cli-0.7.0}/README.md +3 -1
  3. {kiteml_cli-0.6.0 → kiteml_cli-0.7.0}/src/kite_cli/__init__.py +1 -1
  4. {kiteml_cli-0.6.0 → kiteml_cli-0.7.0}/src/kite_cli/cli/commands/evaluations.py +36 -7
  5. {kiteml_cli-0.6.0 → kiteml_cli-0.7.0}/src/kite_cli/cli/commands/twin.py +19 -11
  6. {kiteml_cli-0.6.0 → kiteml_cli-0.7.0}/src/kite_cli/mcp/server.py +19 -17
  7. {kiteml_cli-0.6.0 → kiteml_cli-0.7.0}/.gitignore +0 -0
  8. {kiteml_cli-0.6.0 → kiteml_cli-0.7.0}/pyproject.toml +0 -0
  9. {kiteml_cli-0.6.0 → kiteml_cli-0.7.0}/src/kite_cli/archive.py +0 -0
  10. {kiteml_cli-0.6.0 → kiteml_cli-0.7.0}/src/kite_cli/auth.py +0 -0
  11. {kiteml_cli-0.6.0 → kiteml_cli-0.7.0}/src/kite_cli/cli/__init__.py +0 -0
  12. {kiteml_cli-0.6.0 → kiteml_cli-0.7.0}/src/kite_cli/cli/commands/__init__.py +0 -0
  13. {kiteml_cli-0.6.0 → kiteml_cli-0.7.0}/src/kite_cli/cli/commands/augment.py +0 -0
  14. {kiteml_cli-0.6.0 → kiteml_cli-0.7.0}/src/kite_cli/cli/commands/auth_cmd.py +0 -0
  15. {kiteml_cli-0.6.0 → kiteml_cli-0.7.0}/src/kite_cli/cli/commands/config_cmd.py +0 -0
  16. {kiteml_cli-0.6.0 → kiteml_cli-0.7.0}/src/kite_cli/cli/commands/rl.py +0 -0
  17. {kiteml_cli-0.6.0 → kiteml_cli-0.7.0}/src/kite_cli/cli/main.py +0 -0
  18. {kiteml_cli-0.6.0 → kiteml_cli-0.7.0}/src/kite_cli/client.py +0 -0
  19. {kiteml_cli-0.6.0 → kiteml_cli-0.7.0}/src/kite_cli/config.py +0 -0
  20. {kiteml_cli-0.6.0 → kiteml_cli-0.7.0}/src/kite_cli/mcp/__init__.py +0 -0
  21. {kiteml_cli-0.6.0 → kiteml_cli-0.7.0}/src/kite_cli/mcp/common.py +0 -0
  22. {kiteml_cli-0.6.0 → kiteml_cli-0.7.0}/src/kite_cli/output.py +0 -0
  23. {kiteml_cli-0.6.0 → kiteml_cli-0.7.0}/src/kite_cli/progress.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: kiteml-cli
3
- Version: 0.6.0
3
+ Version: 0.7.0
4
4
  Summary: Kite CLI and MCP server: train robot policies with reinforcement learning in simulation, augment LeRobot datasets, and build MuJoCo digital twins, from your terminal or an AI agent.
5
5
  Project-URL: Homepage, https://kiteml.com
6
6
  Project-URL: Documentation, https://docs.kiteml.com/tools/cli-and-mcp
@@ -150,7 +150,9 @@ kite eval run specs/smoke.yaml --out ./runs # -> ./runs/eval_.../scorecard.js
150
150
 
151
151
  A file too big to send, such as a checkpoint's weights, can come from the Hugging Face Hub or a public S3 bucket:
152
152
  `--hf PATH=OWNER/NAME@COMMIT:FILE` or `--hf PATH=<the file's huggingface.co URL>`, and `--s3 PATH=s3://BUCKET/KEY`,
153
- PATH being the path the spec names.
153
+ PATH being the path the spec names. A checkpoint of one of your Kite training runs goes as
154
+ `--training-run PATH=trn_...[@STEP]` (its latest without `@STEP`), PATH being the checkpoint folder the spec names:
155
+ Kite reads it from its own storage and publishes nothing.
154
156
 
155
157
  Every command prints JSON (`{"ok": true, "data": ...}`) so scripts and agents can parse it. Run `kite --help` for
156
158
  the full list.
@@ -108,7 +108,9 @@ kite eval run specs/smoke.yaml --out ./runs # -> ./runs/eval_.../scorecard.js
108
108
 
109
109
  A file too big to send, such as a checkpoint's weights, can come from the Hugging Face Hub or a public S3 bucket:
110
110
  `--hf PATH=OWNER/NAME@COMMIT:FILE` or `--hf PATH=<the file's huggingface.co URL>`, and `--s3 PATH=s3://BUCKET/KEY`,
111
- PATH being the path the spec names.
111
+ PATH being the path the spec names. A checkpoint of one of your Kite training runs goes as
112
+ `--training-run PATH=trn_...[@STEP]` (its latest without `@STEP`), PATH being the checkpoint folder the spec names:
113
+ Kite reads it from its own storage and publishes nothing.
112
114
 
113
115
  Every command prints JSON (`{"ok": true, "data": ...}`) so scripts and agents can parse it. Run `kite --help` for
114
116
  the full list.
@@ -1,3 +1,3 @@
1
1
  """Kite CLI and MCP server for the Kite robotics training platform."""
2
2
 
3
- __version__ = "0.6.0"
3
+ __version__ = "0.7.0"
@@ -17,7 +17,8 @@ SPEC is the file `kite run` takes (kite_spec_version "2"). The command sends its
17
17
  names (`files_required`), and sends each from this disk: a file or folder Kite ships by its sha256 alone, anything
18
18
  else inline (about 15 MB in all). A file on the Hugging Face Hub, such as a checkpoint's weights, goes as a
19
19
  reference: `--hf PATH=OWNER/NAME@COMMIT:FILE`, PATH as the spec names it (a folder's file as FOLDER/FILE); an object
20
- in a public S3 bucket as `--s3 PATH=s3://BUCKET/KEY`.
20
+ in a public S3 bucket as `--s3 PATH=s3://BUCKET/KEY`; a checkpoint of one of your Kite training runs as
21
+ `--training-run PATH=trn_...[@STEP]`, PATH being the folder the spec names (`policy.asset_dir`).
21
22
  Where runs go: --out DIR, else $KITE_EVAL_RUNS_DIR, else ./runs (`~` works). Paths printed are absolute. A run
22
23
  directory extracts to <out>/<run_id>/; the SDK's `kite verify <out>/<run_id> --evals DIR` checks it, DIR being the
23
24
  evaluators the spec named (the run records where its job read them, not your folder).
@@ -55,12 +56,16 @@ _BODY_MAX = 24_000_000 # base64 characters in all, under the 32 MiB a request c
55
56
  _ROUNDS = 64 # validate calls while the files are worked out: one per missing path or unshipped hash, at most
56
57
  _HF = re.compile(r"(?P<repo>[A-Za-z0-9][A-Za-z0-9._-]*/[A-Za-z0-9][A-Za-z0-9._-]*)@(?P<revision>[0-9a-f]{40})"
57
58
  r":(?P<path>.+)")
59
+ _TRN = re.compile(r"(?P<run>trn_[0-9A-HJKMNP-TV-Z]{26})(@(?P<step>[0-9]+))?") # a training run's id, as the API has it
58
60
 
59
61
  _SET_HELP = "FIELD=VALUE, as `kite run --set` (repeatable)"
60
62
  _HF_HELP = ("PATH=OWNER/NAME@COMMIT:FILE, or PATH=URL (https://huggingface.co/OWNER/NAME/blob/REVISION/FILE; Kite pins a "
61
63
  "branch to its commit): the spec's PATH is that file of a public Hugging Face repo (repeatable)")
62
64
  _S3_HELP = ("PATH=s3://BUCKET/KEY (or its https://BUCKET.s3.amazonaws.com/KEY URL): the spec's PATH is that object of a "
63
65
  "public S3 bucket, pinned to the object it is when the run is created (repeatable)")
66
+ _TRN_HELP = ("PATH=trn_...[@STEP]: the spec's PATH (a folder, e.g. policy.asset_dir) is the pretrained_model/ folder "
67
+ "of one of your training runs' checkpoints, at STEP or its latest; read from Kite's storage, nothing is "
68
+ "published (repeatable)")
64
69
 
65
70
 
66
71
  @app.command()
@@ -69,12 +74,13 @@ def validate(
69
74
  set_: Optional[List[str]] = typer.Option(None, "--set", help=_SET_HELP),
70
75
  hf: Optional[List[str]] = typer.Option(None, "--hf", help=_HF_HELP),
71
76
  s3: Optional[List[str]] = typer.Option(None, "--s3", help=_S3_HELP),
77
+ training_run: Optional[List[str]] = typer.Option(None, "--training-run", help=_TRN_HELP),
72
78
  kite_version: Optional[str] = typer.Option(None, "--kite-version", help="The Kite release you expect"),
73
79
  ):
74
80
  """Free dry run of a run: the spec plans, every file it names is sent, and the price. Creates nothing."""
75
81
  try:
76
82
  client = KiteClient(timeout=_SLOW_S)
77
- _, validation, sent = request(client, spec, set_ or [], {**hf_refs(hf or []), **s3_refs(s3 or [])}, kite_version)
83
+ _, validation, sent = request(client, spec, set_ or [], remote_refs(hf, s3, training_run), kite_version)
78
84
  print_json({"ok": True, "data": validation, "files": sent})
79
85
  except KiteAPIError as e:
80
86
  print_error(e.message, code=e.code, details=e.details)
@@ -86,6 +92,7 @@ def run(
86
92
  set_: Optional[List[str]] = typer.Option(None, "--set", help=_SET_HELP),
87
93
  hf: Optional[List[str]] = typer.Option(None, "--hf", help=_HF_HELP),
88
94
  s3: Optional[List[str]] = typer.Option(None, "--s3", help=_S3_HELP),
95
+ training_run: Optional[List[str]] = typer.Option(None, "--training-run", help=_TRN_HELP),
89
96
  max_minutes: Optional[int] = typer.Option(
90
97
  None, "--max-minutes", help="The run's cap, 1-720: what it reserves and the most it can cost "
91
98
  "(default 60 on CPU, 120 on an L4)"),
@@ -106,7 +113,7 @@ def run(
106
113
  key = idempotency_key or str(uuid.uuid4())
107
114
  try:
108
115
  client = KiteClient(timeout=_SLOW_S)
109
- body, _, _ = request(client, spec, set_ or [], {**hf_refs(hf or []), **s3_refs(s3 or [])}, kite_version)
116
+ body, _, _ = request(client, spec, set_ or [], remote_refs(hf, s3, training_run), kite_version)
110
117
  options = {k: v for k, v in (("max_minutes", max_minutes), ("no_eval", no_eval),
111
118
  ("verify_determinism", verify_determinism)) if v}
112
119
  body = {**body, **({"options": options} if options else {}), **({"label": label} if label else {})}
@@ -251,8 +258,8 @@ def resume(run_id: str = typer.Argument(..., help="Evaluation id (eval_...)")):
251
258
  def request(client: KiteClient, spec: Path, overrides: List[str], remote: Dict[str, dict],
252
259
  kite_version: Optional[str] = None) -> Tuple[dict, dict, Dict[str, str]]:
253
260
  """The request body for SPEC with every path it names, the validation that accepted it, and how each path went
254
- (`sha256`, `inline`, `hf`, `s3`). `remote` maps the paths that are an `hf` file or an `s3` object to their
255
- reference ({"hf": …} or {"s3": …}).
261
+ (`sha256`, `inline`, `hf`, `s3`, `training_run`). `remote` maps the paths that are an `hf` file, an `s3` object
262
+ or a training run's checkpoint folder to their reference ({"hf": …}, {"s3": …} or {"training_run": …}).
256
263
 
257
264
  The API names what it lacks: `files_required` lists the paths the spec names that the request does not carry,
258
265
  and `file_unavailable` on `files.<path>` says a hash Kite does not ship. A path first goes by its hash alone, so
@@ -263,7 +270,8 @@ def request(client: KiteClient, spec: Path, overrides: List[str], remote: Dict[s
263
270
  for key, ref in remote.items():
264
271
  files[key] = dict(ref)
265
272
  local = _local(base, key)
266
- if local.is_file(): # pin the bytes this machine has: the Hub's file or the object must be the same
273
+ # pin the bytes this machine has: the Hub's file or the object must be the same (Kite pins a checkpoint's)
274
+ if local.is_file() and "training_run" not in ref:
267
275
  files[key]["sha256"] = _file_sha256(local)
268
276
  sent[key] = next(iter(ref))
269
277
  body: dict = {"spec_yaml": text, "spec_path": spec_path, "files": files}
@@ -364,7 +372,8 @@ def _fits(key: str, raw_bytes: int, files: Dict[str, dict], biggest: List[Tuple[
364
372
  hint = "".join(f", {posixpath.join(key, rel)} {p.stat().st_size >> 20} MB" for rel, p in biggest)
365
373
  raise KiteAPIError(413, f"{key} is not a file Kite ships, and it is too big to send inline "
366
374
  f"({raw_bytes >> 20} MB{hint}): put its large files on the Hugging Face Hub or in a public "
367
- "S3 bucket and pass each with --hf PATH=OWNER/NAME@COMMIT:FILE or --s3 PATH=s3://BUCKET/KEY", code="file_too_large",
375
+ "S3 bucket and pass each with --hf PATH=OWNER/NAME@COMMIT:FILE or --s3 PATH=s3://BUCKET/KEY (a "
376
+ "checkpoint Kite trained for you: --training-run PATH=trn_…@STEP)", code="file_too_large",
368
377
  details={"path": key})
369
378
 
370
379
 
@@ -433,6 +442,26 @@ def s3_refs(pairs: List[str]) -> Dict[str, dict]:
433
442
  return refs
434
443
 
435
444
 
445
+ def training_run_refs(pairs: List[str]) -> Dict[str, dict]:
446
+ """PATH=trn_…[@STEP] pairs as {"training_run": id, "step": STEP} references, the step left out for the run's
447
+ latest checkpoint; the API checks the run is the caller's."""
448
+ refs: Dict[str, dict] = {}
449
+ for pair in pairs:
450
+ key, _, ref = pair.partition("=")
451
+ m = _TRN.fullmatch(ref.strip())
452
+ if not key.strip() or not m:
453
+ raise KiteAPIError(400, f"--training-run {pair!r}: expected PATH=trn_…[@STEP], a training run's id "
454
+ "(trn_ and 26 characters) and optionally the step of one of its checkpoints",
455
+ code="parameter_invalid", details={"param": "training_run"})
456
+ refs[key.strip()] = {"training_run": m["run"], **({"step": int(m["step"])} if m["step"] else {})}
457
+ return refs
458
+
459
+
460
+ def remote_refs(hf: Optional[List[str]], s3: Optional[List[str]], training_run: Optional[List[str]]) -> Dict[str, dict]:
461
+ """The --hf, --s3 and --training-run pairs as one map of path to reference."""
462
+ return {**hf_refs(hf or []), **s3_refs(s3 or []), **training_run_refs(training_run or [])}
463
+
464
+
436
465
  # --- downloads ------------------------------------------------------------------------------------------------------
437
466
 
438
467
  def _page(limit: int, after: Optional[str]) -> dict:
@@ -2,6 +2,7 @@
2
2
  MuJoCo scene out (about 100 minutes per twin).
3
3
 
4
4
  kite twin validate lerobot/svla_so101_pickplace [--camera side] free: cameras, episodes, time and price
5
+ kite twin validate s3://my-bucket/my_dataset --robot so101 a dataset in a public S3 bucket
5
6
  kite twin create lerobot/svla_so101_pickplace --wait --out ./twins -> ./twins/twin_.../scene.xml
6
7
  kite twin status twin_01K... [--wait]
7
8
  kite twin list [--limit 20] [--after twin_...]
@@ -10,7 +11,8 @@ MuJoCo scene out (about 100 minutes per twin).
10
11
  kite twin resume twin_01K...
11
12
  kite twin download twin_01K... [--out DIR] [--no-extract]
12
13
 
13
- DATASET is a Hugging Face repo id; the API takes it as {"source": {"type": "huggingface", "repo_id": DATASET}}.
14
+ DATASET is a Hugging Face repo id or URL, which the API takes as {"source": {"type": "huggingface", "repo_id": DATASET}},
15
+ or an S3 link to the dataset's folder in a public bucket, taken as {"source": {"type": "s3", "uri": DATASET}}.
14
16
  Where twins go: --out DIR, else $KITE_TWINS_DIR, else ./twins (`~` works). Paths printed are absolute.
15
17
 
16
18
  A signed storage URL is fetched with a plain HTTP client, never with the Kite key; the API's own archive
@@ -21,6 +23,7 @@ and unpacked only inside <out>/<twin_id>/.
21
23
  from __future__ import annotations
22
24
 
23
25
  import os
26
+ import re
24
27
  import uuid
25
28
  from pathlib import Path, PurePosixPath
26
29
  from typing import Optional
@@ -38,11 +41,13 @@ app = typer.Typer(no_args_is_help=True, help="Digital twins: one robot episode -
38
41
  _TERMINAL = {"succeeded", "failed", "canceled"} # not canceling: the cancel was accepted, the build has not stopped
39
42
  _POLL_S = 30.0
40
43
  _SLOW_S = 180.0 # validate/create check the dataset server-side (the API allows the builder 120 s)
44
+ _DATASET_HELP = ("Hugging Face dataset id or URL, e.g. lerobot/svla_so101_pickplace, or an S3 link to the dataset's "
45
+ "folder in a public bucket, s3://BUCKET/PREFIX")
41
46
 
42
47
 
43
48
  @app.command()
44
49
  def validate(
45
- dataset: str = typer.Argument(..., help="Hugging Face dataset id or URL, e.g. lerobot/svla_so101_pickplace"),
50
+ dataset: str = typer.Argument(..., help=_DATASET_HELP),
46
51
  camera: Optional[str] = typer.Option(None, "--camera", help="Substring of the camera that seeds the room"),
47
52
  wrist_camera: Optional[str] = typer.Option(None, "--wrist-camera", help="Substring of the wrist camera"),
48
53
  robot: Optional[str] = typer.Option(None, "--robot", help="Also check this robot can drive the dataset"),
@@ -50,7 +55,7 @@ def validate(
50
55
  """Free dry run of a create: cameras, episodes, fps, and a time estimate. Creates nothing."""
51
56
  try:
52
57
  client = KiteClient(timeout=_SLOW_S)
53
- body = _body(source=_source(dataset), camera=camera, wrist_camera=wrist_camera, robot=robot)
58
+ body = _body(source=twin_source(dataset), camera=camera, wrist_camera=wrist_camera, robot=robot)
54
59
  print_json(client.post("/v1/twins/validate", json_data=body))
55
60
  except KiteAPIError as e:
56
61
  print_error(e.message, code=e.code, details=e.details)
@@ -58,8 +63,8 @@ def validate(
58
63
 
59
64
  @app.command()
60
65
  def create(
61
- dataset: str = typer.Argument(..., help="Hugging Face dataset id or URL, e.g. lerobot/svla_so101_pickplace"),
62
- robot: str = typer.Option(..., "--robot", help="The robot in the episode: so100, so101, koch, aloha, wx250s, wxai_biarm, piper, xarm7, panda, panda_ee_state, g1_dex3"),
66
+ dataset: str = typer.Argument(..., help=_DATASET_HELP),
67
+ robot: str = typer.Option(..., "--robot", help="The robot in the episode: so100, so101, koch, aloha, wx250s, wxai_biarm, piper, xarm7, panda, panda_ee_state, g1_dex3, vega_1p"),
63
68
  name: Optional[str] = typer.Option(None, "--name", help="A short name for the twin"),
64
69
  task: Optional[str] = typer.Option(None, "--task", help="Which objects to author: the ones the robot manipulates"),
65
70
  prompt: Optional[str] = typer.Option(None, "--prompt", help="How to describe the room to the reconstructor"),
@@ -77,10 +82,10 @@ def create(
77
82
  "default location for `kite twin download` is $KITE_TWINS_DIR, else ./twins"
78
83
  ),
79
84
  ):
80
- """Build a twin from one episode of a LeRobot dataset and its robot (queued; a few hours; 5,000 tokens, refunded
81
- if it builds nothing)."""
85
+ """Build a twin from one episode of a LeRobot dataset and its robot (queued; a few hours; 25,000 tokens, refunded
86
+ if it ends before authoring)."""
82
87
  key = idempotency_key or str(uuid.uuid4())
83
- body = _body(source=_source(dataset), robot=robot, name=name, task=task, prompt=prompt, camera=camera,
88
+ body = _body(source=twin_source(dataset), robot=robot, name=name, task=task, prompt=prompt, camera=camera,
84
89
  wrist_camera=wrist_camera, ceiling_m=ceiling_m)
85
90
  try:
86
91
  client = KiteClient(timeout=_SLOW_S)
@@ -179,7 +184,7 @@ def cancel(twin_id: str = typer.Argument(..., help="Twin id (twin_...)")):
179
184
  @app.command()
180
185
  def resume(twin_id: str = typer.Argument(..., help="Twin id (twin_...)")):
181
186
  """Re-queue a failed or canceled twin; only the stages that did not finish run again. Free while it's paid for;
182
- a twin that was refunded is charged 5,000 tokens again."""
187
+ a twin that was refunded is charged 25,000 tokens again."""
183
188
  try:
184
189
  print_json(KiteClient().post(f"/v1/twins/{twin_id}/resume"))
185
190
  except KiteAPIError as e:
@@ -206,8 +211,11 @@ def _body(**fields) -> dict:
206
211
  return {k: v for k, v in fields.items() if v not in (None, "")}
207
212
 
208
213
 
209
- def _source(dataset: str) -> dict:
210
- """The positional DATASET as the API takes it: the `source` object /v1/augmentations takes too."""
214
+ def twin_source(dataset: str) -> dict:
215
+ """DATASET as the API's `source`: an S3 link (s3://BUCKET/PREFIX, or its https://….amazonaws.com URL) is its
216
+ `uri`, anything else a Hugging Face `repo_id`. The API checks either."""
217
+ if re.match(r"(?i)\s*(s3://|https?://[^/]*amazonaws\.com/)", dataset):
218
+ return {"type": "s3", "uri": dataset.strip()}
211
219
  return {"type": "huggingface", "repo_id": dataset}
212
220
 
213
221
 
@@ -51,11 +51,6 @@ def _client() -> KiteClient:
51
51
  return KiteClient(api_url=os.environ.get("KITE_API_URL"), token=token, surface="kite-mcp")
52
52
 
53
53
 
54
- def _twin_source(dataset: str) -> dict:
55
- """The tools take `dataset`, the simplest thing to ask an agent for; the API takes it as a `source`."""
56
- return {"type": "huggingface", "repo_id": dataset}
57
-
58
-
59
54
  def _fetch_command(d: dict, fname: str, client: KiteClient) -> str:
60
55
  """The command that fetches a finished resource's archive. From Kite-Version 2026-10-12 `download.url` is this
61
56
  API's path, which needs the key: ask it for the fresh signed link it redirects to, so an agent without the key
@@ -140,10 +135,10 @@ def _eval_request(args: dict, client: KiteClient) -> tuple[dict, dict | None]:
140
135
  raise ValueError("`spec` only works with the local (stdio) Kite MCP server; send `spec_yaml` and `files`")
141
136
  from pathlib import Path
142
137
 
143
- from kite_cli.cli.commands.evaluations import hf_refs, request, s3_refs
138
+ from kite_cli.cli.commands.evaluations import remote_refs, request
144
139
 
145
- remote = {**hf_refs([f"{k}={v}" for k, v in (args.get("hf") or {}).items()]),
146
- **s3_refs([f"{k}={v}" for k, v in (args.get("s3") or {}).items()])}
140
+ pairs = {m: [f"{k}={v}" for k, v in (args.get(m) or {}).items()] for m in ("hf", "s3", "training_runs")}
141
+ remote = remote_refs(pairs["hf"], pairs["s3"], pairs["training_runs"])
147
142
  body, validation, _ = request(client, Path(args["spec"]), args.get("overrides") or [], remote, args.get("kite_version"))
148
143
  return body, validation
149
144
 
@@ -204,14 +199,19 @@ _EVAL_SPEC = {
204
199
  "spec": {"type": "string", "description": "A Kite SDK spec file on this machine, best as an absolute path (the local server only): every path it names is sent from this disk, a file Kite ships by its hash alone"},
205
200
  "spec_yaml": {"type": "string", "description": "The spec's text, instead of `spec` (the hosted server): send `spec_path` and `files` with it"},
206
201
  "spec_path": {"type": "string", "description": "With spec_yaml: its path in your workspace, e.g. specs/smoke.yaml. Paths in the spec are read relative to it"},
207
- "files": {"type": "object", "description": "With spec_yaml: each path the spec names, as it names it, to where its bytes come from: {\"kind\": \"directory\", \"shipped\": \"evals\"} (a path Kite ships), {\"sha256\", \"kind\"} (a shipped file by hash), {\"text\"} or {\"base64\"} (a folder as a .tar), or {\"hf\": {\"repo\", \"revision\" (a full commit), \"path\"}} or {\"hf\": \"<the file's huggingface.co URL>\"} (a branch is pinned to its commit), or {\"s3\": \"s3://BUCKET/KEY\"} (an object in a public bucket, pinned to its ETag). A missing one is files_required, listing each"},
202
+ "files": {"type": "object", "description": "With spec_yaml: each path the spec names, as it names it, to where its bytes come from: {\"kind\": \"directory\", \"shipped\": \"evals\"} (a path Kite ships), {\"sha256\", \"kind\"} (a shipped file by hash), {\"text\"} or {\"base64\"} (a folder as a .tar), or {\"hf\": {\"repo\", \"revision\" (a full commit), \"path\"}} or {\"hf\": \"<the file's huggingface.co URL>\"} (a branch is pinned to its commit), or {\"s3\": \"s3://BUCKET/KEY\"} (an object in a public bucket, pinned to its ETag), or {\"training_run\": \"trn_...\", \"step\"} (a folder: the pretrained_model/ of one of your training runs' checkpoints, step left out for its latest; nothing is published). A missing one is files_required, listing each"},
208
203
  "overrides": {"type": "array", "items": {"type": "string"}, "description": "FIELD=VALUE pairs, as `kite run --set`, e.g. episodes.count=20. Optional"},
209
204
  "hf": {"type": "object", "description": "With spec: {PATH: \"OWNER/NAME@COMMIT:FILE\" or the file's huggingface.co URL}, the paths that are a public Hugging Face file (a checkpoint's weights). Optional"},
210
205
  "s3": {"type": "object", "description": "With spec: {PATH: \"s3://BUCKET/KEY\"}, the paths that are an object in a public S3 bucket. Optional"},
206
+ "training_runs": {"type": "object", "description": "With spec: {PATH: \"trn_...\" or \"trn_...@STEP\"}, the folders (policy.asset_dir) that are the pretrained_model/ of one of your training runs' checkpoints, its latest without @STEP; read from Kite's storage, nothing is published. Optional"},
211
207
  "kite_version": {"type": "string", "description": "The Kite release you expect; refused if it is not the one served. Optional"},
212
208
  }
213
209
  _EVAL_ID = {"run_id": {"type": "string", "description": "Evaluation id (eval_...)"}}
214
210
 
211
+ # The twin tools take `dataset`, the simplest thing to ask an agent for, and send it as the API's `source`.
212
+ _TWIN_DATASET = ("HF dataset id or URL, e.g. lerobot/svla_so101_pickplace (the API's source.repo_id), or an S3 link to "
213
+ "the dataset's folder in a public bucket, s3://BUCKET/PREFIX (source.uri)")
214
+
215
215
  # ---------------------------------------------------------------------------
216
216
  # Tool definitions — every tool has title + annotations per Claude directory
217
217
  # ---------------------------------------------------------------------------
@@ -265,17 +265,17 @@ TOOLS = [
265
265
 
266
266
  # Digital twins (public /v1 API): one robot episode -> an interactable MuJoCo scene, ~100 min each
267
267
  Tool(name="kite_twin_validate", title="Validate Digital Twin Dataset",
268
- description="Free check before building a twin: confirms a HuggingFace LeRobot dataset resolves and has camera video, says which camera will seed the room, and returns the episodes, fps, typical duration and price (estimate.tokens). Creates nothing. Call this first; errors name what to fix (dataset_not_found, camera_not_found with the available cameras, no_suitable_camera).",
268
+ description="Free check before building a twin: confirms a LeRobot dataset (on HuggingFace, or in a public S3 bucket) resolves and has camera video, says which camera will seed the room, and returns the episodes, fps, typical duration and price (estimate.tokens). Creates nothing. Call this first; errors name what to fix (dataset_not_found, dataset_private for an S3 bucket that is not public, camera_not_found with the available cameras, no_suitable_camera).",
269
269
  inputSchema={"type": "object", "properties": {
270
- "dataset": {"type": "string", "description": "HF dataset id or URL, e.g. lerobot/svla_so101_pickplace (the API's source.repo_id)"},
270
+ "dataset": {"type": "string", "description": _TWIN_DATASET},
271
271
  "camera": {"type": "string", "description": "Substring of the camera that should seed the room. Optional"},
272
272
  "robot": {"type": "string", "description": "Also check this robot can drive the dataset (see kite_twin_create). Optional"}},
273
273
  "required": ["dataset"]}, annotations=_READ),
274
274
  Tool(name="kite_twin_create", title="Create Digital Twin",
275
- description="Build an interactable MuJoCo digital twin from one episode of a HuggingFace LeRobot dataset: the room is reconstructed, the manipulated objects are authored with working joints, and every stage is reviewed by measurement. `dataset` and `robot` are required; the twin reports the dataset back as source.repo_id. Takes a few hours and costs 5,000 tokens, charged now and refunded in full if the build ends before any stage finishes (insufficient_tokens when the balance is short); returns a queued twin (id twin_...) to poll with kite_twin_status.",
275
+ description="Build an interactable MuJoCo digital twin from one episode of a LeRobot dataset, on HuggingFace or in a public S3 bucket: the room is reconstructed, the manipulated objects are authored with working joints, and every stage is reviewed by measurement. `dataset` and `robot` are required; the twin reports the dataset back as source.repo_id (source.uri for S3). Takes a few hours and costs 25,000 tokens, charged now and refunded in full if the build ends before authoring begins (insufficient_tokens when the balance is short); returns a queued twin (id twin_...) to poll with kite_twin_status.",
276
276
  inputSchema={"type": "object", "properties": {
277
- "dataset": {"type": "string", "description": "HF dataset id or URL, e.g. lerobot/svla_so101_pickplace (the API's source.repo_id)"},
278
- "robot": {"type": "string", "description": "The robot in the episode: so100, so101, koch, aloha, wx250s, wxai_biarm, piper, xarm7, panda, panda_ee_state or g1_dex3 (robot_mismatch lists the ones that fit the dataset)"},
277
+ "dataset": {"type": "string", "description": _TWIN_DATASET},
278
+ "robot": {"type": "string", "description": "The robot in the episode: so100, so101, koch, aloha, wx250s, wxai_biarm, piper, xarm7, panda, panda_ee_state, g1_dex3 or vega_1p (robot_mismatch lists the ones that fit the dataset)"},
279
279
  "name": {"type": "string", "description": "Short name for the twin. Optional"},
280
280
  "task": {"type": "string", "description": "Which objects to author (the things the robot manipulates). Optional"},
281
281
  "camera": {"type": "string", "description": "Substring of the camera that seeds the room. Optional"},
@@ -296,7 +296,7 @@ TOOLS = [
296
296
  "twin_id": {"type": "string", "description": "Twin id (twin_...)"}},
297
297
  "required": ["twin_id"]}, annotations=_DESTRUCTIVE),
298
298
  Tool(name="kite_twin_resume", title="Resume Digital Twin",
299
- description="Re-queue a failed or canceled twin. Every stage it finished is cached, so only what did not finish runs again. Free while the twin is paid for; one that was refunded (it built nothing) is charged 5,000 tokens again.",
299
+ description="Re-queue a failed or canceled twin. Every stage it finished is cached, so only what did not finish runs again. Free while the twin is paid for; one that was refunded (it ended before authoring) is charged 25,000 tokens again.",
300
300
  inputSchema={"type": "object", "properties": {
301
301
  "twin_id": {"type": "string", "description": "Twin id (twin_...)"}},
302
302
  "required": ["twin_id"]}, annotations=_WRITE),
@@ -466,11 +466,13 @@ async def _dispatch(name: str, args: dict, client: KiteClient) -> list[TextConte
466
466
  return _ok(client.post(f"/v1/augmentations/{args['aug_id']}/cancel"))
467
467
 
468
468
  # --- Digital twins (public /v1 API) ---
469
+ from kite_cli.cli.commands.twin import twin_source
470
+
469
471
  if name == "kite_twin_validate":
470
- body = {"source": _twin_source(args["dataset"]), **{k: args[k] for k in ("camera", "robot") if args.get(k)}}
472
+ body = {"source": twin_source(args["dataset"]), **{k: args[k] for k in ("camera", "robot") if args.get(k)}}
471
473
  return _ok(client.post("/v1/twins/validate", json_data=body))
472
474
  if name == "kite_twin_create":
473
- body = {"source": _twin_source(args["dataset"]),
475
+ body = {"source": twin_source(args["dataset"]),
474
476
  **{k: args[k] for k in ("robot", "name", "task", "camera") if args.get(k)}}
475
477
  headers = {"Idempotency-Key": args["idempotency_key"]} if args.get("idempotency_key") else None
476
478
  return _ok(_twin_view(client.post("/v1/twins", json_data=body, extra_headers=headers), client))
File without changes
File without changes