Aksel Joonas Reedi commited on
Commit
6131fc8
·
unverified ·
1 Parent(s): f9305f6

Embed trackio dashboard in chat for jobs and sandboxes (#161)

Browse files

* Embed trackio dashboard in chat for jobs and sandboxes

Wires `trackio_space_id` / `trackio_project` through hf_jobs and
sandbox_create as optional tool args, then renders the resulting
dashboard inline as an embedded iframe in the chat.

The plumbing turned out to need four pieces, not two:
1. Inject TRACKIO_SPACE_ID / TRACKIO_PROJECT into job + sandbox env.
2. Surface the values in tool_state_change so the frontend can render
the embed when the tool call is in flight.
3. Pre-seed the Space with our own README/requirements/app.py — without
this, trackio's init() against an existing empty Space leaves the
iframe stuck on the default Gradio template. We omit `hf_oauth: true`
from the seeded README so the embed renders without a sign-in click;
per-user privacy comes from HF namespace ownership.
4. Mount the metrics bucket on the dashboard Space at /data and set
TRACKIO_DIR / TRACKIO_BUCKET_ID. trackio.init() in the job creates
the bucket but only attaches it to the job process — without
mounting it on the Space too, the dashboard reads an empty /data
and shows "No projects" even though the bucket has data.

The seed helper (`agent/tools/trackio_seed.py`) is idempotent and runs
synchronously before run_job / sandbox_create whenever a trackio_space_id
is provided. Bumps `huggingface-hub` to >= 1.12.0 for the bucket / volume
APIs (`create_bucket`, `Volume`, `set_space_volumes`, `add_space_variable`).

Validated end-to-end with a real trackio job against
akseljoonas/mlintern-trackio-pragmatic-01 — dashboard renders project,
runs, and loss/accuracy charts with no sign-in prompt.

* Brand seeded trackio dashboards with the ml-intern logo

Sets TRACKIO_LOGO_LIGHT_URL / TRACKIO_LOGO_DARK_URL on every seeded Space
to the smolagents.webp hosted on the smolagents/ml-intern Space, so the
dashboard renders ml-intern branding instead of the trackio wordmark.
Idempotent — only writes the variable when the value would change.

TRACKIO_THEME deliberately not set: trackio dropped Gradio-theme support
in 1.x, so the variable is a no-op there.

* Show loading state in trackio embed until iframe paints

A freshly-seeded HF Space takes 30–60s to build, and even after build the
trackio bundle takes a few seconds to render its first frame — during
that whole window the iframe was a blank white rectangle with no signal
that anything was happening.

Stack a 'Spinning up the dashboard…' overlay (spinner + short hint about
typical build time) on top of the iframe and clear it once the iframe's
load event fires.

* Match trackio embed loading state to chat dark theme

- Force trackio dashboard into dark mode via gradio's __theme=dark query
param so the embedded charts blend with the chat instead of flashing
a white panel after load.
- Switch the iframe container's loading background from white to
var(--code-panel-bg) so any frame between iframe paint and load uses
the same dark surface as the surrounding chat.
- Reword the placeholder copy to talk about the trackio dashboard
rather than HF Spaces — that's the abstraction users care about.

* Sandbox trackio iframe, validate space ID, persist dashboards

Address PR review:
- iframe gets a sandbox attribute so the embedded gradio app can't reach
back into the parent (forms, scripts, same-origin, popups, downloads,
modals are all the runtime needs).
- spaceIdToSubdomain only runs on validated repo IDs; an unexpected value
now suppresses the embed instead of building a malformed URL that
could redirect to an attacker-controlled subdomain.
- trackioDashboards mirrors toolErrors/rejectedTools and persists to
localStorage so the dashboard survives a page refresh during a run.

agent/tools/jobs_tool.py CHANGED
@@ -19,6 +19,7 @@ from huggingface_hub.utils import HfHubHTTPError
19
 
20
  from agent.core.hf_access import JobsAccessError, resolve_jobs_namespace
21
  from agent.core.session import Event
 
22
  from agent.tools.types import ToolResult
23
 
24
  logger = logging.getLogger(__name__)
@@ -382,6 +383,31 @@ class HfJobsTool:
382
  "isError": True,
383
  }
384
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
385
  async def _wait_for_job_completion(
386
  self, job_id: str, namespace: Optional[str] = None
387
  ) -> tuple[str, list[str]]:
@@ -533,11 +559,24 @@ class HfJobsTool:
533
  # Run the job
534
  flavor = args.get("hardware_flavor", "cpu-basic")
535
  timeout_str = args.get("timeout", "30m")
 
 
 
 
 
 
 
 
 
 
 
 
 
536
  job = await _async_call(
537
  self.api.run_job,
538
  image=image,
539
  command=command,
540
- env=_add_default_env(args.get("env")),
541
  secrets=_add_environment_variables(args.get("secrets"), self.hf_token),
542
  flavor=flavor,
543
  timeout=timeout_str,
@@ -550,16 +589,18 @@ class HfJobsTool:
550
 
551
  # Send job URL immediately after job creation (before waiting for completion)
552
  if self.session and self.tool_call_id:
 
 
 
 
 
 
 
 
 
 
553
  await self.session.send_event(
554
- Event(
555
- event_type="tool_state_change",
556
- data={
557
- "tool_call_id": self.tool_call_id,
558
- "tool": "hf_jobs",
559
- "state": "running",
560
- "jobUrl": job.url,
561
- },
562
- )
563
  )
564
 
565
  # Telemetry: job submission + completion (infra consumption signal).
@@ -594,16 +635,18 @@ class HfJobsTool:
594
 
595
  # Notify frontend of final status
596
  if self.session and self.tool_call_id:
 
 
 
 
 
 
 
 
 
 
597
  await self.session.send_event(
598
- Event(
599
- event_type="tool_state_change",
600
- data={
601
- "tool_call_id": self.tool_call_id,
602
- "tool": "hf_jobs",
603
- "state": final_status.lower(),
604
- "jobUrl": job.url,
605
- },
606
- )
607
  )
608
 
609
  # Filter out UV package installation output
@@ -977,7 +1020,10 @@ HF_JOBS_TOOL_SPEC = {
977
  "- You MUST have validated dataset format via hf_inspect_dataset or hub_repo_details.\n"
978
  "- Training config MUST include push_to_hub=True and hub_model_id. "
979
  "Job storage is EPHEMERAL — all files are deleted when the job ends. Without push_to_hub, trained models are lost permanently.\n"
980
- "- Include trackio monitoring and provide the dashboard URL to the user.\n\n"
 
 
 
981
  "BATCH/ABLATION JOBS: Submit ONE job first. Check logs to confirm it starts training successfully. "
982
  "Only then submit the remaining jobs. Never submit all at once — if there's a bug, all jobs fail.\n\n"
983
  "Operations: run, ps, logs, inspect, cancel, scheduled run/ps/inspect/delete/suspend/resume.\n\n"
@@ -1060,6 +1106,26 @@ HF_JOBS_TOOL_SPEC = {
1060
  "type": "object",
1061
  "description": "Environment variables {'KEY': 'VALUE'}. HF_TOKEN is auto-included.",
1062
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1063
  "namespace": {
1064
  "type": "string",
1065
  "description": (
 
19
 
20
  from agent.core.hf_access import JobsAccessError, resolve_jobs_namespace
21
  from agent.core.session import Event
22
+ from agent.tools.trackio_seed import ensure_trackio_dashboard
23
  from agent.tools.types import ToolResult
24
 
25
  logger = logging.getLogger(__name__)
 
383
  "isError": True,
384
  }
385
 
386
+ async def _seed_trackio_dashboard(self, space_id: str) -> None:
387
+ """Idempotently install trackio dashboard files into *space_id* before
388
+ the job runs. Surfaces seed progress as tool_log events but never
389
+ raises — a seed failure should not block job submission, since trackio
390
+ often still works when the Space already has dashboard code from a
391
+ previous run.
392
+ """
393
+ loop = asyncio.get_running_loop()
394
+
395
+ def _log(msg: str) -> None:
396
+ if self.session is None:
397
+ return
398
+ loop.call_soon_threadsafe(
399
+ self.session.event_queue.put_nowait,
400
+ Event(event_type="tool_log", data={"tool": "hf_jobs", "log": msg}),
401
+ )
402
+
403
+ try:
404
+ await asyncio.to_thread(
405
+ ensure_trackio_dashboard, space_id, self.hf_token, _log
406
+ )
407
+ except Exception as e:
408
+ logger.warning(f"trackio dashboard seed failed for {space_id}: {e}")
409
+ _log(f"trackio dashboard seed failed: {e}")
410
+
411
  async def _wait_for_job_completion(
412
  self, job_id: str, namespace: Optional[str] = None
413
  ) -> tuple[str, list[str]]:
 
559
  # Run the job
560
  flavor = args.get("hardware_flavor", "cpu-basic")
561
  timeout_str = args.get("timeout", "30m")
562
+
563
+ # Trackio: agent-declared space + project become env vars on the job
564
+ # so trackio.init() picks them up automatically. We also surface them
565
+ # in tool_state_change so the frontend can embed the dashboard.
566
+ env_dict = _add_default_env(args.get("env"))
567
+ trackio_space_id = args.get("trackio_space_id")
568
+ trackio_project = args.get("trackio_project")
569
+ if trackio_space_id:
570
+ env_dict["TRACKIO_SPACE_ID"] = trackio_space_id
571
+ await self._seed_trackio_dashboard(trackio_space_id)
572
+ if trackio_project:
573
+ env_dict["TRACKIO_PROJECT"] = trackio_project
574
+
575
  job = await _async_call(
576
  self.api.run_job,
577
  image=image,
578
  command=command,
579
+ env=env_dict,
580
  secrets=_add_environment_variables(args.get("secrets"), self.hf_token),
581
  flavor=flavor,
582
  timeout=timeout_str,
 
589
 
590
  # Send job URL immediately after job creation (before waiting for completion)
591
  if self.session and self.tool_call_id:
592
+ state_data: Dict[str, Any] = {
593
+ "tool_call_id": self.tool_call_id,
594
+ "tool": "hf_jobs",
595
+ "state": "running",
596
+ "jobUrl": job.url,
597
+ }
598
+ if trackio_space_id:
599
+ state_data["trackioSpaceId"] = trackio_space_id
600
+ if trackio_project:
601
+ state_data["trackioProject"] = trackio_project
602
  await self.session.send_event(
603
+ Event(event_type="tool_state_change", data=state_data)
 
 
 
 
 
 
 
 
604
  )
605
 
606
  # Telemetry: job submission + completion (infra consumption signal).
 
635
 
636
  # Notify frontend of final status
637
  if self.session and self.tool_call_id:
638
+ final_data: Dict[str, Any] = {
639
+ "tool_call_id": self.tool_call_id,
640
+ "tool": "hf_jobs",
641
+ "state": final_status.lower(),
642
+ "jobUrl": job.url,
643
+ }
644
+ if trackio_space_id:
645
+ final_data["trackioSpaceId"] = trackio_space_id
646
+ if trackio_project:
647
+ final_data["trackioProject"] = trackio_project
648
  await self.session.send_event(
649
+ Event(event_type="tool_state_change", data=final_data)
 
 
 
 
 
 
 
 
650
  )
651
 
652
  # Filter out UV package installation output
 
1020
  "- You MUST have validated dataset format via hf_inspect_dataset or hub_repo_details.\n"
1021
  "- Training config MUST include push_to_hub=True and hub_model_id. "
1022
  "Job storage is EPHEMERAL — all files are deleted when the job ends. Without push_to_hub, trained models are lost permanently.\n"
1023
+ "- Include trackio monitoring and provide the dashboard URL to the user. "
1024
+ "When the script uses report_to='trackio', also pass `trackio_space_id` "
1025
+ "(e.g. '<username>/mlintern-<8char>') and `trackio_project` as tool args — "
1026
+ "they are injected as TRACKIO_SPACE_ID/TRACKIO_PROJECT env vars and let the UI embed the live dashboard.\n\n"
1027
  "BATCH/ABLATION JOBS: Submit ONE job first. Check logs to confirm it starts training successfully. "
1028
  "Only then submit the remaining jobs. Never submit all at once — if there's a bug, all jobs fail.\n\n"
1029
  "Operations: run, ps, logs, inspect, cancel, scheduled run/ps/inspect/delete/suspend/resume.\n\n"
 
1106
  "type": "object",
1107
  "description": "Environment variables {'KEY': 'VALUE'}. HF_TOKEN is auto-included.",
1108
  },
1109
+ "trackio_space_id": {
1110
+ "type": "string",
1111
+ "description": (
1112
+ "Optional. The HF Space hosting the trackio dashboard for this run "
1113
+ "(e.g. '<username>/mlintern-<8char>', under YOUR HF namespace). "
1114
+ "Injected as TRACKIO_SPACE_ID env var and used by the UI to embed "
1115
+ "the live dashboard. Set this whenever the script uses "
1116
+ "report_to='trackio'. The Space is auto-created and seeded with the "
1117
+ "trackio dashboard before the job starts — DO NOT pre-create it via "
1118
+ "hf_repo_git, that produces an empty Space that breaks the embed."
1119
+ ),
1120
+ },
1121
+ "trackio_project": {
1122
+ "type": "string",
1123
+ "description": (
1124
+ "Optional. The trackio project name to log this run under. "
1125
+ "Injected as TRACKIO_PROJECT env var and used by the UI to filter "
1126
+ "the embedded dashboard to this project."
1127
+ ),
1128
+ },
1129
  "namespace": {
1130
  "type": "string",
1131
  "description": (
agent/tools/sandbox_tool.py CHANGED
@@ -19,6 +19,7 @@ from huggingface_hub import HfApi, SpaceHardware
19
 
20
  from agent.core.session import Event
21
  from agent.tools.sandbox_client import Sandbox
 
22
 
23
 
24
  def _looks_like_path(script: str) -> bool:
@@ -62,11 +63,36 @@ async def resolve_sandbox_script(
62
  return None, f"Failed to read {script} from sandbox: {e}"
63
 
64
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
  # ── Tool name mapping (short agent names → Sandbox client names) ──────
66
 
67
 
68
  async def _ensure_sandbox(
69
- session: Any, hardware: str = "cpu-basic", **create_kwargs
 
 
 
70
  ) -> tuple[Sandbox | None, str | None]:
71
  """
72
  Ensure a sandbox exists on the session. Auto-creates with given hardware if needed.
@@ -120,11 +146,15 @@ async def _ensure_sandbox(
120
 
121
  watcher_task = asyncio.create_task(_watch_cancel())
122
 
 
 
 
 
123
  kwargs = {
124
  "owner": owner,
125
  "hardware": hardware,
126
  "token": token,
127
- "secrets": {"HF_TOKEN": token},
128
  "log": _log,
129
  "cancel_event": cancel_flag,
130
  **create_kwargs,
@@ -188,6 +218,9 @@ SANDBOX_CREATE_TOOL_SPEC = {
188
  "fp32 ≈ 4 bytes/param, plus ~20% overhead for optimizer states during training.\n"
189
  "Common picks: t4-small (16GB VRAM, fits ≤1-3B), a10g-small (24GB, ≤7B), a100-large (80GB, ≤30B). "
190
  "If the model won't fit, pick larger hardware upfront — OOM on a sandbox wastes time.\n\n"
 
 
 
191
  "Hardware: " + ", ".join([e.value for e in SpaceHardware]) + ".\n"
192
  ),
193
  "parameters": {
@@ -204,16 +237,49 @@ SANDBOX_CREATE_TOOL_SPEC = {
204
  "type": "boolean",
205
  "description": "If true, create a private Space",
206
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
207
  },
208
  },
209
  }
210
 
211
 
212
  async def sandbox_create_handler(
213
- args: dict[str, Any], session: Any = None
214
  ) -> tuple[str, bool]:
215
  """Handle sandbox_create tool calls."""
216
  hardware = args.get("hardware", "cpu-basic")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
217
 
218
  # If sandbox already exists, return its info
219
  if session and getattr(session, "sandbox", None):
@@ -226,6 +292,7 @@ async def sandbox_create_handler(
226
  "Hardware cannot be changed by calling sandbox_create again. "
227
  "Delete the existing sandbox first if you need a different tier."
228
  )
 
229
  return (
230
  f"Sandbox already active: {sb.space_id}\n"
231
  f"URL: {sb.url}\n"
@@ -233,18 +300,32 @@ async def sandbox_create_handler(
233
  f"Use bash/read/write/edit to interact with it."
234
  ), True
235
 
236
- create_kwargs = {}
237
  if "private" in args:
238
  create_kwargs["private"] = args["private"]
239
 
 
 
 
 
 
 
 
240
  try:
241
- sb, error = await _ensure_sandbox(session, hardware=hardware, **create_kwargs)
 
 
 
 
 
242
  except Exception as e:
243
  return f"Failed to create sandbox: {e}", False
244
 
245
  if error:
246
  return error, False
247
 
 
 
248
  return (
249
  f"Sandbox created: {sb.space_id}\n"
250
  f"URL: {sb.url}\n"
 
19
 
20
  from agent.core.session import Event
21
  from agent.tools.sandbox_client import Sandbox
22
+ from agent.tools.trackio_seed import ensure_trackio_dashboard
23
 
24
 
25
  def _looks_like_path(script: str) -> bool:
 
63
  return None, f"Failed to read {script} from sandbox: {e}"
64
 
65
 
66
+ async def _seed_trackio_dashboard_safe(session: Any, space_id: str) -> None:
67
+ """Idempotently seed *space_id* with trackio dashboard files using the
68
+ session's HF token. Logs progress, swallows errors — a failed seed should
69
+ not block sandbox creation."""
70
+ if not session or not getattr(session, "hf_token", None):
71
+ return
72
+ loop = asyncio.get_running_loop()
73
+
74
+ def _log(msg: str) -> None:
75
+ loop.call_soon_threadsafe(
76
+ session.event_queue.put_nowait,
77
+ Event(event_type="tool_log", data={"tool": "sandbox_create", "log": msg}),
78
+ )
79
+
80
+ try:
81
+ await asyncio.to_thread(
82
+ ensure_trackio_dashboard, space_id, session.hf_token, _log
83
+ )
84
+ except Exception as e:
85
+ _log(f"trackio dashboard seed failed: {e}")
86
+
87
+
88
  # ── Tool name mapping (short agent names → Sandbox client names) ──────
89
 
90
 
91
  async def _ensure_sandbox(
92
+ session: Any,
93
+ hardware: str = "cpu-basic",
94
+ extra_secrets: dict[str, str] | None = None,
95
+ **create_kwargs,
96
  ) -> tuple[Sandbox | None, str | None]:
97
  """
98
  Ensure a sandbox exists on the session. Auto-creates with given hardware if needed.
 
146
 
147
  watcher_task = asyncio.create_task(_watch_cancel())
148
 
149
+ secrets: dict[str, str] = {"HF_TOKEN": token}
150
+ if extra_secrets:
151
+ secrets.update({k: v for k, v in extra_secrets.items() if v})
152
+
153
  kwargs = {
154
  "owner": owner,
155
  "hardware": hardware,
156
  "token": token,
157
+ "secrets": secrets,
158
  "log": _log,
159
  "cancel_event": cancel_flag,
160
  **create_kwargs,
 
218
  "fp32 ≈ 4 bytes/param, plus ~20% overhead for optimizer states during training.\n"
219
  "Common picks: t4-small (16GB VRAM, fits ≤1-3B), a10g-small (24GB, ≤7B), a100-large (80GB, ≤30B). "
220
  "If the model won't fit, pick larger hardware upfront — OOM on a sandbox wastes time.\n\n"
221
+ "If you intend to run a training script in this sandbox that uses report_to='trackio', "
222
+ "pass `trackio_space_id` (e.g. '<username>/mlintern-<8char>') and `trackio_project` so they "
223
+ "are set as TRACKIO_SPACE_ID/TRACKIO_PROJECT secrets in the sandbox and the UI can embed the live dashboard.\n\n"
224
  "Hardware: " + ", ".join([e.value for e in SpaceHardware]) + ".\n"
225
  ),
226
  "parameters": {
 
237
  "type": "boolean",
238
  "description": "If true, create a private Space",
239
  },
240
+ "trackio_space_id": {
241
+ "type": "string",
242
+ "description": (
243
+ "Optional. The HF Space hosting the trackio dashboard for runs in this sandbox "
244
+ "(e.g. '<username>/mlintern-<8char>', under YOUR HF namespace). Injected as "
245
+ "TRACKIO_SPACE_ID secret and surfaced to the UI. The Space is auto-created and "
246
+ "seeded with the trackio dashboard — DO NOT pre-create it via hf_repo_git, "
247
+ "that produces an empty Space that breaks the embed."
248
+ ),
249
+ },
250
+ "trackio_project": {
251
+ "type": "string",
252
+ "description": (
253
+ "Optional. The trackio project name. Injected as TRACKIO_PROJECT secret and "
254
+ "used by the UI to filter the embedded dashboard to this project."
255
+ ),
256
+ },
257
  },
258
  },
259
  }
260
 
261
 
262
  async def sandbox_create_handler(
263
+ args: dict[str, Any], session: Any = None, tool_call_id: str | None = None
264
  ) -> tuple[str, bool]:
265
  """Handle sandbox_create tool calls."""
266
  hardware = args.get("hardware", "cpu-basic")
267
+ trackio_space_id = args.get("trackio_space_id") or None
268
+ trackio_project = args.get("trackio_project") or None
269
+
270
+ async def _emit_trackio_state(sb: Sandbox) -> None:
271
+ """Tell the frontend which trackio dashboard to embed for this sandbox."""
272
+ if not (session and tool_call_id and trackio_space_id):
273
+ return
274
+ data: dict[str, Any] = {
275
+ "tool_call_id": tool_call_id,
276
+ "tool": "sandbox_create",
277
+ "state": "running",
278
+ "trackioSpaceId": trackio_space_id,
279
+ }
280
+ if trackio_project:
281
+ data["trackioProject"] = trackio_project
282
+ await session.send_event(Event(event_type="tool_state_change", data=data))
283
 
284
  # If sandbox already exists, return its info
285
  if session and getattr(session, "sandbox", None):
 
292
  "Hardware cannot be changed by calling sandbox_create again. "
293
  "Delete the existing sandbox first if you need a different tier."
294
  )
295
+ await _emit_trackio_state(sb)
296
  return (
297
  f"Sandbox already active: {sb.space_id}\n"
298
  f"URL: {sb.url}\n"
 
300
  f"Use bash/read/write/edit to interact with it."
301
  ), True
302
 
303
+ create_kwargs: dict[str, Any] = {}
304
  if "private" in args:
305
  create_kwargs["private"] = args["private"]
306
 
307
+ extra_secrets: dict[str, str] = {}
308
+ if trackio_space_id:
309
+ extra_secrets["TRACKIO_SPACE_ID"] = trackio_space_id
310
+ await _seed_trackio_dashboard_safe(session, trackio_space_id)
311
+ if trackio_project:
312
+ extra_secrets["TRACKIO_PROJECT"] = trackio_project
313
+
314
  try:
315
+ sb, error = await _ensure_sandbox(
316
+ session,
317
+ hardware=hardware,
318
+ extra_secrets=extra_secrets or None,
319
+ **create_kwargs,
320
+ )
321
  except Exception as e:
322
  return f"Failed to create sandbox: {e}", False
323
 
324
  if error:
325
  return error, False
326
 
327
+ await _emit_trackio_state(sb)
328
+
329
  return (
330
  f"Sandbox created: {sb.space_id}\n"
331
  f"URL: {sb.url}\n"
agent/tools/trackio_seed.py ADDED
@@ -0,0 +1,205 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Seed an HF Space with the trackio dashboard.
2
+
3
+ Background: when the agent creates a Space via `hf_repo_git create_repo` (or
4
+ the user pre-creates one), it ships with no app.py — so the iframe shows the
5
+ default Gradio "Get started" template instead of charts. Trackio's `init()`
6
+ detects the existing Space but does NOT auto-bootstrap dashboard files into it,
7
+ so the dashboard never materializes.
8
+
9
+ This helper writes the three files trackio's runtime expects (README.md,
10
+ requirements.txt, app.py) into the Space, idempotently, BEFORE the job that
11
+ will call `trackio.init()` runs. We deliberately omit `hf_oauth: true` from
12
+ the README so the embedded iframe in ml-intern renders without a login click —
13
+ per-user privacy is enforced by namespace ownership instead.
14
+
15
+ Beyond the dashboard files, the helper also creates the metrics bucket and
16
+ mounts it on the Space at `/data` (with `TRACKIO_DIR` / `TRACKIO_BUCKET_ID`
17
+ Space variables). Without this, the running job writes metrics into a bucket
18
+ that the dashboard Space can't read, and the iframe shows "No projects".
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import io
24
+ from typing import Callable, Optional
25
+
26
+ from huggingface_hub import (
27
+ HfApi,
28
+ Volume,
29
+ add_space_variable,
30
+ create_bucket,
31
+ create_repo,
32
+ )
33
+ from huggingface_hub.utils import EntryNotFoundError, RepositoryNotFoundError
34
+
35
+
36
+ _README = """---
37
+ title: Trackio Dashboard
38
+ emoji: 📊
39
+ colorFrom: pink
40
+ colorTo: gray
41
+ sdk: gradio
42
+ app_file: app.py
43
+ pinned: false
44
+ tags:
45
+ - trackio
46
+ ---
47
+
48
+ Embedded trackio dashboard for ml-intern runs.
49
+ """
50
+
51
+ _REQUIREMENTS = "trackio\n"
52
+ _APP_PY = "import trackio\ntrackio.show()\n"
53
+
54
+ # ml-intern brand mark surfaced inside the trackio dashboard. Trackio reads
55
+ # `TRACKIO_LOGO_LIGHT_URL` / `TRACKIO_LOGO_DARK_URL` from Space variables and
56
+ # renders them in place of its own logo. We point at the publicly-resolvable
57
+ # copy on the smolagents/ml-intern Space repo so any seeded dashboard inherits
58
+ # the ml-intern branding without each user having to host the asset.
59
+ _LOGO_URL = (
60
+ "https://huggingface.co/spaces/smolagents/ml-intern/"
61
+ "resolve/main/frontend/public/smolagents.webp"
62
+ )
63
+
64
+ _FILES = {
65
+ "README.md": _README,
66
+ "requirements.txt": _REQUIREMENTS,
67
+ "app.py": _APP_PY,
68
+ }
69
+
70
+
71
+ def _already_seeded(api: HfApi, space_id: str) -> bool:
72
+ """Cheap check: does the Space already have a trackio dashboard app.py?
73
+
74
+ Avoids re-uploading the same three files on every job submission. We look
75
+ for the literal `trackio.show` call which is the load-bearing line — any
76
+ other app.py shape (the default gradio shell, a stale custom one) means
77
+ we should re-seed.
78
+ """
79
+ try:
80
+ path = api.hf_hub_download(
81
+ repo_id=space_id, repo_type="space", filename="app.py"
82
+ )
83
+ except (EntryNotFoundError, RepositoryNotFoundError, OSError):
84
+ return False
85
+ try:
86
+ with open(path, "r", encoding="utf-8") as f:
87
+ return "trackio.show" in f.read()
88
+ except OSError:
89
+ return False
90
+
91
+
92
+ def _get_space_volumes(api: HfApi, space_id: str) -> list:
93
+ """Return mounted volumes for a Space.
94
+
95
+ `get_space_runtime()` doesn't always populate `volumes` even when the
96
+ mount exists; mirror trackio's fallback to `space_info().runtime.volumes`.
97
+ """
98
+ runtime = api.get_space_runtime(space_id)
99
+ if getattr(runtime, "volumes", None):
100
+ return list(runtime.volumes)
101
+ info = api.space_info(space_id)
102
+ if info.runtime and getattr(info.runtime, "volumes", None):
103
+ return list(info.runtime.volumes)
104
+ return []
105
+
106
+
107
+ def _ensure_bucket_mounted(
108
+ api: HfApi,
109
+ space_id: str,
110
+ bucket_id: str,
111
+ hf_token: str,
112
+ log: Optional[Callable[[str], None]] = None,
113
+ ) -> None:
114
+ """Create the bucket if missing, mount it at `/data` on the Space, and
115
+ set the `TRACKIO_DIR` / `TRACKIO_BUCKET_ID` Space variables. Idempotent —
116
+ skips work that has already been done.
117
+ """
118
+ create_bucket(bucket_id, private=True, exist_ok=True, token=hf_token)
119
+
120
+ existing = _get_space_volumes(api, space_id)
121
+ already_mounted = any(
122
+ getattr(v, "type", None) == "bucket"
123
+ and getattr(v, "source", None) == bucket_id
124
+ and getattr(v, "mount_path", None) == "/data"
125
+ for v in existing
126
+ )
127
+ if not already_mounted:
128
+ preserved = [
129
+ v
130
+ for v in existing
131
+ if not (
132
+ getattr(v, "type", None) == "bucket"
133
+ and (
134
+ getattr(v, "source", None) == bucket_id
135
+ or getattr(v, "mount_path", None) == "/data"
136
+ )
137
+ )
138
+ ]
139
+ api.set_space_volumes(
140
+ space_id,
141
+ preserved + [Volume(type="bucket", source=bucket_id, mount_path="/data")],
142
+ )
143
+ if log:
144
+ log(f"mounted bucket {bucket_id} at /data on {space_id}")
145
+
146
+ variables = api.get_space_variables(space_id)
147
+ desired = {
148
+ "TRACKIO_DIR": "/data/trackio",
149
+ "TRACKIO_BUCKET_ID": bucket_id,
150
+ "TRACKIO_LOGO_LIGHT_URL": _LOGO_URL,
151
+ "TRACKIO_LOGO_DARK_URL": _LOGO_URL,
152
+ }
153
+ for key, value in desired.items():
154
+ if getattr(variables.get(key), "value", None) != value:
155
+ add_space_variable(space_id, key, value, token=hf_token)
156
+
157
+
158
+ def ensure_trackio_dashboard(
159
+ space_id: str,
160
+ hf_token: str,
161
+ log: Optional[Callable[[str], None]] = None,
162
+ ) -> bool:
163
+ """Make sure *space_id* is fully wired for trackio:
164
+ 1. Space exists with our dashboard files (README without `hf_oauth`,
165
+ `requirements.txt`, `app.py` calling `trackio.show`).
166
+ 2. Bucket `<space_id>-bucket` exists, is mounted at `/data`, and the
167
+ Space has `TRACKIO_DIR` / `TRACKIO_BUCKET_ID` variables set.
168
+
169
+ Idempotent — re-running is cheap. Returns True if any seeding happened
170
+ in step (1), False if the dashboard files were already in place. Bucket
171
+ mount is always re-checked.
172
+ """
173
+ api = HfApi(token=hf_token)
174
+
175
+ create_repo(
176
+ repo_id=space_id,
177
+ repo_type="space",
178
+ space_sdk="gradio",
179
+ exist_ok=True,
180
+ token=hf_token,
181
+ )
182
+
183
+ seeded_files = False
184
+ if _already_seeded(api, space_id):
185
+ if log:
186
+ log(f"trackio dashboard already seeded on {space_id}")
187
+ else:
188
+ if log:
189
+ log(f"seeding trackio dashboard files into {space_id}")
190
+ for path_in_repo, content in _FILES.items():
191
+ api.upload_file(
192
+ path_or_fileobj=io.BytesIO(content.encode("utf-8")),
193
+ path_in_repo=path_in_repo,
194
+ repo_id=space_id,
195
+ repo_type="space",
196
+ commit_message=f"ml-intern: seed trackio dashboard ({path_in_repo})",
197
+ )
198
+ seeded_files = True
199
+
200
+ bucket_id = f"{space_id}-bucket"
201
+ _ensure_bucket_mounted(api, space_id, bucket_id, hf_token, log)
202
+
203
+ if log:
204
+ log(f"trackio dashboard ready: https://huggingface.co/spaces/{space_id}")
205
+ return seeded_files
frontend/src/components/Chat/ToolCallGroup.tsx CHANGED
@@ -220,6 +220,194 @@ function ResearchSteps({ steps }: { steps: string[] }) {
220
  );
221
  }
222
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
223
  // ---------------------------------------------------------------------------
224
  // Hardware pricing ($/hr) — from HF Spaces & Jobs pricing
225
  // ---------------------------------------------------------------------------
@@ -517,7 +705,7 @@ function InlineApproval({
517
  const EMPTY_AGENTS: Record<string, ResearchAgentState> = {};
518
 
519
  export default function ToolCallGroup({ tools, approveTools }: ToolCallGroupProps) {
520
- const { setPanel, lockPanel, getJobUrl, getEditedScript, setJobStatus, getJobStatus, setToolError, getToolError, setToolRejected, getToolRejected } = useAgentStore();
521
  const researchAgents = useAgentStore(s => {
522
  const activeId = s.activeSessionId;
523
  return (activeId && s.sessionStates[activeId]?.researchAgents) || EMPTY_AGENTS;
@@ -1063,6 +1251,18 @@ export default function ToolCallGroup({ tools, approveTools }: ToolCallGroupProp
1063
  <ResearchSteps steps={researchAgents[tool.toolCallId].steps} />
1064
  )}
1065
 
 
 
 
 
 
 
 
 
 
 
 
 
1066
  {/* Per-tool approval: undecided */}
1067
  {isPending && !localDecision && !isSubmitting && (
1068
  <InlineApproval
 
220
  );
221
  }
222
 
223
+ // ---------------------------------------------------------------------------
224
+ // Trackio dashboard embed
225
+ // ---------------------------------------------------------------------------
226
+
227
+ // HF repo IDs are `<owner>/<name>` where each segment is alphanumerics plus
228
+ // `_`, `.`, `-`. Anything else (slashes, spaces, query params, missing owner)
229
+ // would let an attacker-controlled string redirect the embed to a different
230
+ // Space, so we refuse to render rather than build a malformed URL.
231
+ const SPACE_ID_PATTERN = /^[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+$/;
232
+
233
+ function isValidSpaceId(spaceId: string): boolean {
234
+ return SPACE_ID_PATTERN.test(spaceId);
235
+ }
236
+
237
+ /** HF Space embed subdomain: 'user/space_name' → 'user-space-name'. */
238
+ function spaceIdToSubdomain(spaceId: string): string {
239
+ return spaceId
240
+ .toLowerCase()
241
+ .replace(/[/_.]/g, '-')
242
+ .replace(/-+/g, '-')
243
+ .replace(/^-|-$/g, '');
244
+ }
245
+
246
+ function buildTrackioEmbedUrl(spaceId: string, project?: string): string {
247
+ // __theme=dark is gradio's standard query param to force the embedded
248
+ // dashboard into dark mode so it blends with the surrounding chat instead
249
+ // of flashing a bright white panel inside the dark UI.
250
+ const params = new URLSearchParams({
251
+ sidebar: 'hidden',
252
+ footer: 'false',
253
+ __theme: 'dark',
254
+ });
255
+ if (project) params.set('project', project);
256
+ return `https://${spaceIdToSubdomain(spaceId)}.hf.space/?${params.toString()}`;
257
+ }
258
+
259
+ function buildTrackioPageUrl(spaceId: string, project?: string): string {
260
+ const qs = project ? `?${new URLSearchParams({ project }).toString()}` : '';
261
+ return `https://huggingface.co/spaces/${spaceId}${qs}`;
262
+ }
263
+
264
+ function TrackioEmbed({ spaceId, project }: { spaceId: string; project?: string }) {
265
+ const [expanded, setExpanded] = useState(true);
266
+ const [iframeLoaded, setIframeLoaded] = useState(false);
267
+ const embedUrl = useMemo(() => buildTrackioEmbedUrl(spaceId, project), [spaceId, project]);
268
+ const pageUrl = useMemo(() => buildTrackioPageUrl(spaceId, project), [spaceId, project]);
269
+ const label = project ? `${spaceId} · ${project}` : spaceId;
270
+
271
+ if (!isValidSpaceId(spaceId)) return null;
272
+
273
+ return (
274
+ <Box sx={{ pl: 4.5, pr: 1.5, pb: 1, pt: 0.25 }}>
275
+ <Box
276
+ sx={{
277
+ border: '1px solid var(--tool-border)',
278
+ borderRadius: '8px',
279
+ overflow: 'hidden',
280
+ bgcolor: 'var(--code-panel-bg)',
281
+ }}
282
+ >
283
+ <Stack
284
+ direction="row"
285
+ alignItems="center"
286
+ spacing={1}
287
+ onClick={(e) => e.stopPropagation()}
288
+ sx={{
289
+ px: 1.25,
290
+ py: 0.5,
291
+ borderBottom: expanded ? '1px solid var(--tool-border)' : 'none',
292
+ }}
293
+ >
294
+ <Typography
295
+ sx={{
296
+ fontFamily: '"JetBrains Mono", ui-monospace, SFMono-Regular, monospace',
297
+ fontSize: '0.65rem',
298
+ fontWeight: 600,
299
+ color: 'var(--accent-yellow)',
300
+ letterSpacing: '0.04em',
301
+ }}
302
+ >
303
+ trackio
304
+ </Typography>
305
+ <Typography
306
+ sx={{
307
+ fontFamily: '"JetBrains Mono", ui-monospace, SFMono-Regular, monospace',
308
+ fontSize: '0.65rem',
309
+ color: 'var(--muted-text)',
310
+ flex: 1,
311
+ minWidth: 0,
312
+ overflow: 'hidden',
313
+ textOverflow: 'ellipsis',
314
+ whiteSpace: 'nowrap',
315
+ }}
316
+ >
317
+ {label}
318
+ </Typography>
319
+ <Link
320
+ href={pageUrl}
321
+ target="_blank"
322
+ rel="noopener noreferrer"
323
+ onClick={(e) => e.stopPropagation()}
324
+ sx={{
325
+ display: 'inline-flex',
326
+ alignItems: 'center',
327
+ gap: 0.4,
328
+ color: 'var(--accent-yellow)',
329
+ fontSize: '0.65rem',
330
+ textDecoration: 'none',
331
+ '&:hover': { textDecoration: 'underline' },
332
+ }}
333
+ >
334
+ <LaunchIcon sx={{ fontSize: 11 }} />
335
+ Open
336
+ </Link>
337
+ <Button
338
+ size="small"
339
+ onClick={(e) => {
340
+ e.stopPropagation();
341
+ setExpanded((v) => !v);
342
+ }}
343
+ sx={{
344
+ textTransform: 'none',
345
+ minWidth: 'auto',
346
+ px: 0.75,
347
+ py: 0,
348
+ fontSize: '0.65rem',
349
+ color: 'var(--muted-text)',
350
+ '&:hover': { color: 'var(--text)', bgcolor: 'transparent' },
351
+ }}
352
+ >
353
+ {expanded ? 'Hide' : 'Show'}
354
+ </Button>
355
+ </Stack>
356
+ {expanded && (
357
+ <Box sx={{ position: 'relative', width: '100%', height: 480, bgcolor: 'var(--code-panel-bg)' }}>
358
+ <iframe
359
+ src={embedUrl}
360
+ title={`Trackio dashboard ${label}`}
361
+ loading="lazy"
362
+ onLoad={() => setIframeLoaded(true)}
363
+ sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-downloads allow-modals"
364
+ style={{ border: 0, width: '100%', height: '100%', display: 'block' }}
365
+ />
366
+ {!iframeLoaded && (
367
+ <Stack
368
+ direction="column"
369
+ alignItems="center"
370
+ justifyContent="center"
371
+ spacing={1.5}
372
+ sx={{
373
+ position: 'absolute',
374
+ inset: 0,
375
+ bgcolor: 'var(--code-panel-bg)',
376
+ color: 'var(--muted-text)',
377
+ pointerEvents: 'none',
378
+ }}
379
+ >
380
+ <CircularProgress size={20} sx={{ color: 'var(--accent-yellow)' }} />
381
+ <Typography
382
+ sx={{
383
+ fontFamily: '"JetBrains Mono", ui-monospace, SFMono-Regular, monospace',
384
+ fontSize: '0.75rem',
385
+ color: 'var(--text)',
386
+ }}
387
+ >
388
+ Spinning up the trackio dashboard…
389
+ </Typography>
390
+ <Typography
391
+ sx={{
392
+ fontFamily: '"JetBrains Mono", ui-monospace, SFMono-Regular, monospace',
393
+ fontSize: '0.65rem',
394
+ color: 'var(--muted-text)',
395
+ textAlign: 'center',
396
+ maxWidth: 360,
397
+ px: 2,
398
+ }}
399
+ >
400
+ First load takes 30–60 seconds. Charts appear automatically once the run starts logging.
401
+ </Typography>
402
+ </Stack>
403
+ )}
404
+ </Box>
405
+ )}
406
+ </Box>
407
+ </Box>
408
+ );
409
+ }
410
+
411
  // ---------------------------------------------------------------------------
412
  // Hardware pricing ($/hr) — from HF Spaces & Jobs pricing
413
  // ---------------------------------------------------------------------------
 
705
  const EMPTY_AGENTS: Record<string, ResearchAgentState> = {};
706
 
707
  export default function ToolCallGroup({ tools, approveTools }: ToolCallGroupProps) {
708
+ const { setPanel, lockPanel, getJobUrl, getEditedScript, setJobStatus, getJobStatus, getTrackioDashboard, setToolError, getToolError, setToolRejected, getToolRejected } = useAgentStore();
709
  const researchAgents = useAgentStore(s => {
710
  const activeId = s.activeSessionId;
711
  return (activeId && s.sessionStates[activeId]?.researchAgents) || EMPTY_AGENTS;
 
1251
  <ResearchSteps steps={researchAgents[tool.toolCallId].steps} />
1252
  )}
1253
 
1254
+ {/* Trackio dashboard embed — shown for hf_jobs / sandbox_create runs that declared a trackio space */}
1255
+ {(tool.toolName === 'hf_jobs' || tool.toolName === 'sandbox_create')
1256
+ && !isPending
1257
+ && !isRejected
1258
+ && !cancelled
1259
+ && (() => {
1260
+ const trackio = getTrackioDashboard(tool.toolCallId);
1261
+ return trackio
1262
+ ? <TrackioEmbed spaceId={trackio.spaceId} project={trackio.project} />
1263
+ : null;
1264
+ })()}
1265
+
1266
  {/* Per-tool approval: undecided */}
1267
  {isPending && !localDecision && !isSubmitting && (
1268
  <InlineApproval
frontend/src/lib/sse-chat-transport.ts CHANGED
@@ -226,12 +226,17 @@ function createEventToChunkStream(sideChannel: SideChannelCallbacks): TransformS
226
  const state = (event.data?.state as string) || '';
227
  const toolName = (event.data?.tool as string) || '';
228
  const jobUrl = (event.data?.jobUrl as string) || undefined;
 
 
229
 
230
  if (tcId.startsWith('plan_tool')) break;
231
 
232
  if (jobUrl && tcId) {
233
  useAgentStore.getState().setJobUrl(tcId, jobUrl);
234
  }
 
 
 
235
  if (state === 'running' && toolName) {
236
  sideChannel.onToolRunning(toolName);
237
  }
 
226
  const state = (event.data?.state as string) || '';
227
  const toolName = (event.data?.tool as string) || '';
228
  const jobUrl = (event.data?.jobUrl as string) || undefined;
229
+ const trackioSpaceId = (event.data?.trackioSpaceId as string) || undefined;
230
+ const trackioProject = (event.data?.trackioProject as string) || undefined;
231
 
232
  if (tcId.startsWith('plan_tool')) break;
233
 
234
  if (jobUrl && tcId) {
235
  useAgentStore.getState().setJobUrl(tcId, jobUrl);
236
  }
237
+ if (trackioSpaceId && tcId) {
238
+ useAgentStore.getState().setTrackioDashboard(tcId, trackioSpaceId, trackioProject);
239
+ }
240
  if (state === 'running' && toolName) {
241
  sideChannel.onToolRunning(toolName);
242
  }
frontend/src/store/agentStore.ts CHANGED
@@ -147,6 +147,11 @@ interface AgentStore {
147
  // Job statuses (tool_call_id -> job status) for HF jobs
148
  jobStatuses: Record<string, string>;
149
 
 
 
 
 
 
150
  // Tool error states (tool_call_id -> true if errored) - persisted across renders
151
  toolErrors: Record<string, boolean>;
152
 
@@ -200,6 +205,9 @@ interface AgentStore {
200
  setJobStatus: (toolCallId: string, status: string) => void;
201
  getJobStatus: (toolCallId: string) => string | undefined;
202
 
 
 
 
203
  setToolError: (toolCallId: string, hasError: boolean) => void;
204
  getToolError: (toolCallId: string) => boolean | undefined;
205
 
@@ -264,6 +272,26 @@ function saveRejectedTools(rejected: Record<string, boolean>): void {
264
  }
265
  }
266
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
267
  export const useAgentStore = create<AgentStore>()((set, get) => ({
268
  sessionStates: {},
269
  activeSessionId: null,
@@ -287,6 +315,7 @@ export const useAgentStore = create<AgentStore>()((set, get) => ({
287
  approvalNamespaces: {},
288
  jobUrls: {},
289
  jobStatuses: {},
 
290
  toolErrors: loadToolErrors(),
291
  rejectedTools: loadRejectedTools(),
292
 
@@ -485,6 +514,26 @@ export const useAgentStore = create<AgentStore>()((set, get) => ({
485
 
486
  getJobStatus: (toolCallId) => get().jobStatuses[toolCallId],
487
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
488
  // ── Tool Errors ─────────────────────────────────────────────────────
489
 
490
  setToolError: (toolCallId, hasError) => {
 
147
  // Job statuses (tool_call_id -> job status) for HF jobs
148
  jobStatuses: Record<string, string>;
149
 
150
+ // Trackio dashboard config per tool call (tool_call_id -> {spaceId, project?})
151
+ // Set by hf_jobs / sandbox_create tools when the agent declares trackio_space_id;
152
+ // the UI uses it to embed the live dashboard via an iframe.
153
+ trackioDashboards: Record<string, { spaceId: string; project?: string }>;
154
+
155
  // Tool error states (tool_call_id -> true if errored) - persisted across renders
156
  toolErrors: Record<string, boolean>;
157
 
 
205
  setJobStatus: (toolCallId: string, status: string) => void;
206
  getJobStatus: (toolCallId: string) => string | undefined;
207
 
208
+ setTrackioDashboard: (toolCallId: string, spaceId: string, project?: string) => void;
209
+ getTrackioDashboard: (toolCallId: string) => { spaceId: string; project?: string } | undefined;
210
+
211
  setToolError: (toolCallId: string, hasError: boolean) => void;
212
  getToolError: (toolCallId: string) => boolean | undefined;
213
 
 
272
  }
273
  }
274
 
275
+ // Trackio dashboards survive a page reload — without persistence the iframe
276
+ // disappears whenever the user refreshes mid-job, which is the exact moment
277
+ // they'd want to keep watching it.
278
+ function loadTrackioDashboards(): Record<string, { spaceId: string; project?: string }> {
279
+ try {
280
+ const stored = localStorage.getItem('hf-agent-trackio-dashboards');
281
+ return stored ? JSON.parse(stored) : {};
282
+ } catch {
283
+ return {};
284
+ }
285
+ }
286
+
287
+ function saveTrackioDashboards(dashboards: Record<string, { spaceId: string; project?: string }>): void {
288
+ try {
289
+ localStorage.setItem('hf-agent-trackio-dashboards', JSON.stringify(dashboards));
290
+ } catch (e) {
291
+ console.warn('Failed to persist trackio dashboards:', e);
292
+ }
293
+ }
294
+
295
  export const useAgentStore = create<AgentStore>()((set, get) => ({
296
  sessionStates: {},
297
  activeSessionId: null,
 
315
  approvalNamespaces: {},
316
  jobUrls: {},
317
  jobStatuses: {},
318
+ trackioDashboards: loadTrackioDashboards(),
319
  toolErrors: loadToolErrors(),
320
  rejectedTools: loadRejectedTools(),
321
 
 
514
 
515
  getJobStatus: (toolCallId) => get().jobStatuses[toolCallId],
516
 
517
+ // ── Trackio Dashboards ──────────────────────────────────────────────
518
+
519
+ setTrackioDashboard: (toolCallId, spaceId, project) => {
520
+ set((state) => {
521
+ const existing = state.trackioDashboards[toolCallId];
522
+ // Don't churn the object if nothing changed (avoids extra renders).
523
+ if (existing && existing.spaceId === spaceId && existing.project === project) {
524
+ return {};
525
+ }
526
+ const updated = {
527
+ ...state.trackioDashboards,
528
+ [toolCallId]: { spaceId, ...(project ? { project } : {}) },
529
+ };
530
+ saveTrackioDashboards(updated);
531
+ return { trackioDashboards: updated };
532
+ });
533
+ },
534
+
535
+ getTrackioDashboard: (toolCallId) => get().trackioDashboards[toolCallId],
536
+
537
  // ── Tool Errors ─────────────────────────────────────────────────────
538
 
539
  setToolError: (toolCallId, hasError) => {
pyproject.toml CHANGED
@@ -13,7 +13,7 @@ dependencies = [
13
  "requests>=2.33.0",
14
  "litellm>=1.83.0",
15
  "boto3>=1.35.0",
16
- "huggingface-hub>=1.0.1",
17
  "fastmcp>=3.2.0",
18
  "prompt-toolkit>=3.0.0",
19
  "thefuzz>=0.22.1",
 
13
  "requests>=2.33.0",
14
  "litellm>=1.83.0",
15
  "boto3>=1.35.0",
16
+ "huggingface-hub>=1.12.0",
17
  "fastmcp>=3.2.0",
18
  "prompt-toolkit>=3.0.0",
19
  "thefuzz>=0.22.1",
uv.lock CHANGED
@@ -1006,31 +1006,34 @@ wheels = [
1006
 
1007
  [[package]]
1008
  name = "hf-xet"
1009
- version = "1.2.0"
1010
- source = { registry = "https://pypi.org/simple" }
1011
- sdist = { url = "https://files.pythonhosted.org/packages/5e/6e/0f11bacf08a67f7fb5ee09740f2ca54163863b07b70d579356e9222ce5d8/hf_xet-1.2.0.tar.gz", hash = "sha256:a8c27070ca547293b6890c4bf389f713f80e8c478631432962bb7f4bc0bd7d7f", size = 506020, upload-time = "2025-10-24T19:04:32.129Z" }
1012
- wheels = [
1013
- { url = "https://files.pythonhosted.org/packages/9e/a5/85ef910a0aa034a2abcfadc360ab5ac6f6bc4e9112349bd40ca97551cff0/hf_xet-1.2.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:ceeefcd1b7aed4956ae8499e2199607765fbd1c60510752003b6cc0b8413b649", size = 2861870, upload-time = "2025-10-24T19:04:11.422Z" },
1014
- { url = "https://files.pythonhosted.org/packages/ea/40/e2e0a7eb9a51fe8828ba2d47fe22a7e74914ea8a0db68a18c3aa7449c767/hf_xet-1.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b70218dd548e9840224df5638fdc94bd033552963cfa97f9170829381179c813", size = 2717584, upload-time = "2025-10-24T19:04:09.586Z" },
1015
- { url = "https://files.pythonhosted.org/packages/a5/7d/daf7f8bc4594fdd59a8a596f9e3886133fdc68e675292218a5e4c1b7e834/hf_xet-1.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d40b18769bb9a8bc82a9ede575ce1a44c75eb80e7375a01d76259089529b5dc", size = 3315004, upload-time = "2025-10-24T19:04:00.314Z" },
1016
- { url = "https://files.pythonhosted.org/packages/b1/ba/45ea2f605fbf6d81c8b21e4d970b168b18a53515923010c312c06cd83164/hf_xet-1.2.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:cd3a6027d59cfb60177c12d6424e31f4b5ff13d8e3a1247b3a584bf8977e6df5", size = 3222636, upload-time = "2025-10-24T19:03:58.111Z" },
1017
- { url = "https://files.pythonhosted.org/packages/4a/1d/04513e3cab8f29ab8c109d309ddd21a2705afab9d52f2ba1151e0c14f086/hf_xet-1.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6de1fc44f58f6dd937956c8d304d8c2dea264c80680bcfa61ca4a15e7b76780f", size = 3408448, upload-time = "2025-10-24T19:04:20.951Z" },
1018
- { url = "https://files.pythonhosted.org/packages/f0/7c/60a2756d7feec7387db3a1176c632357632fbe7849fce576c5559d4520c7/hf_xet-1.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f182f264ed2acd566c514e45da9f2119110e48a87a327ca271027904c70c5832", size = 3503401, upload-time = "2025-10-24T19:04:22.549Z" },
1019
- { url = "https://files.pythonhosted.org/packages/4e/64/48fffbd67fb418ab07451e4ce641a70de1c40c10a13e25325e24858ebe5a/hf_xet-1.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:293a7a3787e5c95d7be1857358a9130694a9c6021de3f27fa233f37267174382", size = 2900866, upload-time = "2025-10-24T19:04:33.461Z" },
1020
- { url = "https://files.pythonhosted.org/packages/e2/51/f7e2caae42f80af886db414d4e9885fac959330509089f97cccb339c6b87/hf_xet-1.2.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:10bfab528b968c70e062607f663e21e34e2bba349e8038db546646875495179e", size = 2861861, upload-time = "2025-10-24T19:04:19.01Z" },
1021
- { url = "https://files.pythonhosted.org/packages/6e/1d/a641a88b69994f9371bd347f1dd35e5d1e2e2460a2e350c8d5165fc62005/hf_xet-1.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2a212e842647b02eb6a911187dc878e79c4aa0aa397e88dd3b26761676e8c1f8", size = 2717699, upload-time = "2025-10-24T19:04:17.306Z" },
1022
- { url = "https://files.pythonhosted.org/packages/df/e0/e5e9bba7d15f0318955f7ec3f4af13f92e773fbb368c0b8008a5acbcb12f/hf_xet-1.2.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:30e06daccb3a7d4c065f34fc26c14c74f4653069bb2b194e7f18f17cbe9939c0", size = 3314885, upload-time = "2025-10-24T19:04:07.642Z" },
1023
- { url = "https://files.pythonhosted.org/packages/21/90/b7fe5ff6f2b7b8cbdf1bd56145f863c90a5807d9758a549bf3d916aa4dec/hf_xet-1.2.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:29c8fc913a529ec0a91867ce3d119ac1aac966e098cf49501800c870328cc090", size = 3221550, upload-time = "2025-10-24T19:04:05.55Z" },
1024
- { url = "https://files.pythonhosted.org/packages/6f/cb/73f276f0a7ce46cc6a6ec7d6c7d61cbfe5f2e107123d9bbd0193c355f106/hf_xet-1.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e159cbfcfbb29f920db2c09ed8b660eb894640d284f102ada929b6e3dc410a", size = 3408010, upload-time = "2025-10-24T19:04:28.598Z" },
1025
- { url = "https://files.pythonhosted.org/packages/b8/1e/d642a12caa78171f4be64f7cd9c40e3ca5279d055d0873188a58c0f5fbb9/hf_xet-1.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9c91d5ae931510107f148874e9e2de8a16052b6f1b3ca3c1b12f15ccb491390f", size = 3503264, upload-time = "2025-10-24T19:04:30.397Z" },
1026
- { url = "https://files.pythonhosted.org/packages/17/b5/33764714923fa1ff922770f7ed18c2daae034d21ae6e10dbf4347c854154/hf_xet-1.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:210d577732b519ac6ede149d2f2f34049d44e8622bf14eb3d63bbcd2d4b332dc", size = 2901071, upload-time = "2025-10-24T19:04:37.463Z" },
1027
- { url = "https://files.pythonhosted.org/packages/96/2d/22338486473df5923a9ab7107d375dbef9173c338ebef5098ef593d2b560/hf_xet-1.2.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:46740d4ac024a7ca9b22bebf77460ff43332868b661186a8e46c227fdae01848", size = 2866099, upload-time = "2025-10-24T19:04:15.366Z" },
1028
- { url = "https://files.pythonhosted.org/packages/7f/8c/c5becfa53234299bc2210ba314eaaae36c2875e0045809b82e40a9544f0c/hf_xet-1.2.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:27df617a076420d8845bea087f59303da8be17ed7ec0cd7ee3b9b9f579dff0e4", size = 2722178, upload-time = "2025-10-24T19:04:13.695Z" },
1029
- { url = "https://files.pythonhosted.org/packages/9a/92/cf3ab0b652b082e66876d08da57fcc6fa2f0e6c70dfbbafbd470bb73eb47/hf_xet-1.2.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3651fd5bfe0281951b988c0facbe726aa5e347b103a675f49a3fa8144c7968fd", size = 3320214, upload-time = "2025-10-24T19:04:03.596Z" },
1030
- { url = "https://files.pythonhosted.org/packages/46/92/3f7ec4a1b6a65bf45b059b6d4a5d38988f63e193056de2f420137e3c3244/hf_xet-1.2.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d06fa97c8562fb3ee7a378dd9b51e343bc5bc8190254202c9771029152f5e08c", size = 3229054, upload-time = "2025-10-24T19:04:01.949Z" },
1031
- { url = "https://files.pythonhosted.org/packages/0b/dd/7ac658d54b9fb7999a0ccb07ad863b413cbaf5cf172f48ebcd9497ec7263/hf_xet-1.2.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4c1428c9ae73ec0939410ec73023c4f842927f39db09b063b9482dac5a3bb737", size = 3413812, upload-time = "2025-10-24T19:04:24.585Z" },
1032
- { url = "https://files.pythonhosted.org/packages/92/68/89ac4e5b12a9ff6286a12174c8538a5930e2ed662091dd2572bbe0a18c8a/hf_xet-1.2.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a55558084c16b09b5ed32ab9ed38421e2d87cf3f1f89815764d1177081b99865", size = 3508920, upload-time = "2025-10-24T19:04:26.927Z" },
1033
- { url = "https://files.pythonhosted.org/packages/cb/44/870d44b30e1dcfb6a65932e3e1506c103a8a5aea9103c337e7a53180322c/hf_xet-1.2.0-cp37-abi3-win_amd64.whl", hash = "sha256:e6584a52253f72c9f52f9e549d5895ca7a471608495c4ecaa6cc73dba2b24d69", size = 2905735, upload-time = "2025-10-24T19:04:35.928Z" },
 
 
 
1034
  ]
1035
 
1036
  [[package]]
@@ -1108,7 +1111,7 @@ wheels = [
1108
 
1109
  [[package]]
1110
  name = "huggingface-hub"
1111
- version = "1.1.5"
1112
  source = { registry = "https://pypi.org/simple" }
1113
  dependencies = [
1114
  { name = "filelock" },
@@ -1117,14 +1120,13 @@ dependencies = [
1117
  { name = "httpx" },
1118
  { name = "packaging" },
1119
  { name = "pyyaml" },
1120
- { name = "shellingham" },
1121
  { name = "tqdm" },
1122
- { name = "typer-slim" },
1123
  { name = "typing-extensions" },
1124
  ]
1125
- sdist = { url = "https://files.pythonhosted.org/packages/fb/02/c3d534d7498ba2792da1d2ce56b5d38bbcbcbbba62071c90ee289b408e8d/huggingface_hub-1.1.5.tar.gz", hash = "sha256:40ba5c9a08792d888fde6088920a0a71ab3cd9d5e6617c81a797c657f1fd9968", size = 607199, upload-time = "2025-11-20T15:49:32.809Z" }
1126
  wheels = [
1127
- { url = "https://files.pythonhosted.org/packages/35/f4/124858007ddf3c61e9b144107304c9152fa80b5b6c168da07d86fe583cc1/huggingface_hub-1.1.5-py3-none-any.whl", hash = "sha256:e88ecc129011f37b868586bbcfae6c56868cae80cd56a79d61575426a3aa0d7d", size = 516000, upload-time = "2025-11-20T15:49:30.926Z" },
1128
  ]
1129
 
1130
  [[package]]
@@ -1822,7 +1824,7 @@ requires-dist = [
1822
  { name = "fastapi", specifier = ">=0.115.0" },
1823
  { name = "fastmcp", specifier = ">=3.2.0" },
1824
  { name = "httpx", specifier = ">=0.27.0" },
1825
- { name = "huggingface-hub", specifier = ">=1.0.1" },
1826
  { name = "inspect-ai", marker = "extra == 'eval'", specifier = ">=0.3.149" },
1827
  { name = "litellm", specifier = ">=1.83.0" },
1828
  { name = "ml-intern", extras = ["eval", "dev"], marker = "extra == 'all'" },
@@ -3607,16 +3609,18 @@ wheels = [
3607
  ]
3608
 
3609
  [[package]]
3610
- name = "typer-slim"
3611
- version = "0.20.0"
3612
  source = { registry = "https://pypi.org/simple" }
3613
  dependencies = [
 
3614
  { name = "click" },
3615
- { name = "typing-extensions" },
 
3616
  ]
3617
- sdist = { url = "https://files.pythonhosted.org/packages/8e/45/81b94a52caed434b94da65729c03ad0fb7665fab0f7db9ee54c94e541403/typer_slim-0.20.0.tar.gz", hash = "sha256:9fc6607b3c6c20f5c33ea9590cbeb17848667c51feee27d9e314a579ab07d1a3", size = 106561, upload-time = "2025-10-20T17:03:46.642Z" }
3618
  wheels = [
3619
- { url = "https://files.pythonhosted.org/packages/5e/dd/5cbf31f402f1cc0ab087c94d4669cfa55bd1e818688b910631e131d74e75/typer_slim-0.20.0-py3-none-any.whl", hash = "sha256:f42a9b7571a12b97dddf364745d29f12221865acef7a2680065f9bb29c7dc89d", size = 47087, upload-time = "2025-10-20T17:03:44.546Z" },
3620
  ]
3621
 
3622
  [[package]]
 
1006
 
1007
  [[package]]
1008
  name = "hf-xet"
1009
+ version = "1.4.3"
1010
+ source = { registry = "https://pypi.org/simple" }
1011
+ sdist = { url = "https://files.pythonhosted.org/packages/53/92/ec9ad04d0b5728dca387a45af7bc98fbb0d73b2118759f5f6038b61a57e8/hf_xet-1.4.3.tar.gz", hash = "sha256:8ddedb73c8c08928c793df2f3401ec26f95be7f7e516a7bee2fbb546f6676113", size = 670477, upload-time = "2026-03-31T22:40:07.874Z" }
1012
+ wheels = [
1013
+ { url = "https://files.pythonhosted.org/packages/72/43/724d307b34e353da0abd476e02f72f735cdd2bc86082dee1b32ea0bfee1d/hf_xet-1.4.3-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:7551659ba4f1e1074e9623996f28c3873682530aee0a846b7f2f066239228144", size = 3800935, upload-time = "2026-03-31T22:39:49.618Z" },
1014
+ { url = "https://files.pythonhosted.org/packages/2b/d2/8bee5996b699262edb87dbb54118d287c0e1b2fc78af7cdc41857ba5e3c4/hf_xet-1.4.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:bee693ada985e7045997f05f081d0e12c4c08bd7626dc397f8a7c487e6c04f7f", size = 3558942, upload-time = "2026-03-31T22:39:47.938Z" },
1015
+ { url = "https://files.pythonhosted.org/packages/c3/a1/e993d09cbe251196fb60812b09a58901c468127b7259d2bf0f68bf6088eb/hf_xet-1.4.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:21644b404bb0100fe3857892f752c4d09642586fd988e61501c95bbf44b393a3", size = 4207657, upload-time = "2026-03-31T22:39:39.69Z" },
1016
+ { url = "https://files.pythonhosted.org/packages/64/44/9eb6d21e5c34c63e5e399803a6932fa983cabdf47c0ecbcfe7ea97684b8c/hf_xet-1.4.3-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:987f09cfe418237812896a6736b81b1af02a3a6dcb4b4944425c4c4fca7a7cf8", size = 3986765, upload-time = "2026-03-31T22:39:37.936Z" },
1017
+ { url = "https://files.pythonhosted.org/packages/ea/7b/8ad6f16fdb82f5f7284a34b5ec48645bd575bdcd2f6f0d1644775909c486/hf_xet-1.4.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:60cf7fc43a99da0a853345cf86d23738c03983ee5249613a6305d3e57a5dca74", size = 4188162, upload-time = "2026-03-31T22:39:58.382Z" },
1018
+ { url = "https://files.pythonhosted.org/packages/1b/c4/39d6e136cbeea9ca5a23aad4b33024319222adbdc059ebcda5fc7d9d5ff4/hf_xet-1.4.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2815a49a7a59f3e2edf0cf113ae88e8cb2ca2a221bf353fb60c609584f4884d4", size = 4424525, upload-time = "2026-03-31T22:40:00.225Z" },
1019
+ { url = "https://files.pythonhosted.org/packages/46/f2/adc32dae6bdbc367853118b9878139ac869419a4ae7ba07185dc31251b76/hf_xet-1.4.3-cp313-cp313t-win_amd64.whl", hash = "sha256:42ee323265f1e6a81b0e11094564fb7f7e0ec75b5105ffd91ae63f403a11931b", size = 3671610, upload-time = "2026-03-31T22:40:10.42Z" },
1020
+ { url = "https://files.pythonhosted.org/packages/e2/19/25d897dcc3f81953e0c2cde9ec186c7a0fee413eb0c9a7a9130d87d94d3a/hf_xet-1.4.3-cp313-cp313t-win_arm64.whl", hash = "sha256:27c976ba60079fb8217f485b9c5c7fcd21c90b0367753805f87cb9f3cdc4418a", size = 3528529, upload-time = "2026-03-31T22:40:09.106Z" },
1021
+ { url = "https://files.pythonhosted.org/packages/ec/36/3e8f85ca9fe09b8de2b2e10c63b3b3353d7dda88a0b3d426dffbe7b8313b/hf_xet-1.4.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:5251d5ece3a81815bae9abab41cf7ddb7bcb8f56411bce0827f4a3071c92fdc6", size = 3801019, upload-time = "2026-03-31T22:39:56.651Z" },
1022
+ { url = "https://files.pythonhosted.org/packages/b5/9c/defb6cb1de28bccb7bd8d95f6e60f72a3d3fa4cb3d0329c26fb9a488bfe7/hf_xet-1.4.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1feb0f3abeacee143367c326a128a2e2b60868ec12a36c225afb1d6c5a05e6d2", size = 3558746, upload-time = "2026-03-31T22:39:54.766Z" },
1023
+ { url = "https://files.pythonhosted.org/packages/c1/bd/8d001191893178ff8e826e46ad5299446e62b93cd164e17b0ffea08832ec/hf_xet-1.4.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8b301fc150290ca90b4fccd079829b84bb4786747584ae08b94b4577d82fb791", size = 4207692, upload-time = "2026-03-31T22:39:46.246Z" },
1024
+ { url = "https://files.pythonhosted.org/packages/ce/48/6790b402803250e9936435613d3a78b9aaeee7973439f0918848dde58309/hf_xet-1.4.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:d972fbe95ddc0d3c0fc49b31a8a69f47db35c1e3699bf316421705741aab6653", size = 3986281, upload-time = "2026-03-31T22:39:44.648Z" },
1025
+ { url = "https://files.pythonhosted.org/packages/51/56/ea62552fe53db652a9099eda600b032d75554d0e86c12a73824bfedef88b/hf_xet-1.4.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c5b48db1ee344a805a1b9bd2cda9b6b65fe77ed3787bd6e87ad5521141d317cd", size = 4187414, upload-time = "2026-03-31T22:40:04.951Z" },
1026
+ { url = "https://files.pythonhosted.org/packages/7d/f5/bc1456d4638061bea997e6d2db60a1a613d7b200e0755965ec312dc1ef79/hf_xet-1.4.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:22bdc1f5fb8b15bf2831440b91d1c9bbceeb7e10c81a12e8d75889996a5c9da8", size = 4424368, upload-time = "2026-03-31T22:40:06.347Z" },
1027
+ { url = "https://files.pythonhosted.org/packages/e4/76/ab597bae87e1f06d18d3ecb8ed7f0d3c9a37037fc32ce76233d369273c64/hf_xet-1.4.3-cp314-cp314t-win_amd64.whl", hash = "sha256:0392c79b7cf48418cd61478c1a925246cf10639f4cd9d94368d8ca1e8df9ea07", size = 3672280, upload-time = "2026-03-31T22:40:16.401Z" },
1028
+ { url = "https://files.pythonhosted.org/packages/62/05/2e462d34e23a09a74d73785dbed71cc5dbad82a72eee2ad60a72a554155d/hf_xet-1.4.3-cp314-cp314t-win_arm64.whl", hash = "sha256:681c92a07796325778a79d76c67011764ecc9042a8c3579332b61b63ae512075", size = 3528945, upload-time = "2026-03-31T22:40:14.995Z" },
1029
+ { url = "https://files.pythonhosted.org/packages/ac/9f/9c23e4a447b8f83120798f9279d0297a4d1360bdbf59ef49ebec78fe2545/hf_xet-1.4.3-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:d0da85329eaf196e03e90b84c2d0aca53bd4573d097a75f99609e80775f98025", size = 3805048, upload-time = "2026-03-31T22:39:53.105Z" },
1030
+ { url = "https://files.pythonhosted.org/packages/0b/f8/7aacb8e5f4a7899d39c787b5984e912e6c18b11be136ef13947d7a66d265/hf_xet-1.4.3-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:e23717ce4186b265f69afa66e6f0069fe7efbf331546f5c313d00e123dc84583", size = 3562178, upload-time = "2026-03-31T22:39:51.295Z" },
1031
+ { url = "https://files.pythonhosted.org/packages/df/9a/a24b26dc8a65f0ecc0fe5be981a19e61e7ca963b85e062c083f3a9100529/hf_xet-1.4.3-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc360b70c815bf340ed56c7b8c63aacf11762a4b099b2fe2c9bd6d6068668c08", size = 4212320, upload-time = "2026-03-31T22:39:42.922Z" },
1032
+ { url = "https://files.pythonhosted.org/packages/53/60/46d493db155d2ee2801b71fb1b0fd67696359047fdd8caee2c914cc50c79/hf_xet-1.4.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:39f2d2e9654cd9b4319885733993807aab6de9dfbd34c42f0b78338d6617421f", size = 3991546, upload-time = "2026-03-31T22:39:41.335Z" },
1033
+ { url = "https://files.pythonhosted.org/packages/bc/f5/067363e1c96c6b17256910830d1b54099d06287e10f4ec6ec4e7e08371fc/hf_xet-1.4.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:49ad8a8cead2b56051aa84d7fce3e1335efe68df3cf6c058f22a65513885baac", size = 4193200, upload-time = "2026-03-31T22:40:01.936Z" },
1034
+ { url = "https://files.pythonhosted.org/packages/42/4b/53951592882d9c23080c7644542fda34a3813104e9e11fa1a7d82d419cb8/hf_xet-1.4.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7716d62015477a70ea272d2d68cd7cad140f61c52ee452e133e139abfe2c17ba", size = 4429392, upload-time = "2026-03-31T22:40:03.492Z" },
1035
+ { url = "https://files.pythonhosted.org/packages/8a/21/75a6c175b4e79662ad8e62f46a40ce341d8d6b206b06b4320d07d55b188c/hf_xet-1.4.3-cp37-abi3-win_amd64.whl", hash = "sha256:6b591fcad34e272a5b02607485e4f2a1334aebf1bc6d16ce8eb1eb8978ac2021", size = 3677359, upload-time = "2026-03-31T22:40:13.619Z" },
1036
+ { url = "https://files.pythonhosted.org/packages/8a/7c/44314ecd0e89f8b2b51c9d9e5e7a60a9c1c82024ac471d415860557d3cd8/hf_xet-1.4.3-cp37-abi3-win_arm64.whl", hash = "sha256:7c2c7e20bcfcc946dc67187c203463f5e932e395845d098cc2a93f5b67ca0b47", size = 3533664, upload-time = "2026-03-31T22:40:12.152Z" },
1037
  ]
1038
 
1039
  [[package]]
 
1111
 
1112
  [[package]]
1113
  name = "huggingface-hub"
1114
+ version = "1.12.0"
1115
  source = { registry = "https://pypi.org/simple" }
1116
  dependencies = [
1117
  { name = "filelock" },
 
1120
  { name = "httpx" },
1121
  { name = "packaging" },
1122
  { name = "pyyaml" },
 
1123
  { name = "tqdm" },
1124
+ { name = "typer" },
1125
  { name = "typing-extensions" },
1126
  ]
1127
+ sdist = { url = "https://files.pythonhosted.org/packages/56/52/1b54cb569509c725a32c1315261ac9fd0e6b91bbbf74d86fca10d3376164/huggingface_hub-1.12.0.tar.gz", hash = "sha256:7c3fe85e24b652334e5d456d7a812cd9a071e75630fac4365d9165ab5e4a34b6", size = 763091, upload-time = "2026-04-24T13:32:08.674Z" }
1128
  wheels = [
1129
+ { url = "https://files.pythonhosted.org/packages/7e/2b/ef03ddb96bd1123503c2bd6932001020292deea649e9bf4caa2cb65a85bf/huggingface_hub-1.12.0-py3-none-any.whl", hash = "sha256:d74939969585ee35748bd66de09baf84099d461bda7287cd9043bfb99b0e424d", size = 646806, upload-time = "2026-04-24T13:32:06.717Z" },
1130
  ]
1131
 
1132
  [[package]]
 
1824
  { name = "fastapi", specifier = ">=0.115.0" },
1825
  { name = "fastmcp", specifier = ">=3.2.0" },
1826
  { name = "httpx", specifier = ">=0.27.0" },
1827
+ { name = "huggingface-hub", specifier = ">=1.12.0" },
1828
  { name = "inspect-ai", marker = "extra == 'eval'", specifier = ">=0.3.149" },
1829
  { name = "litellm", specifier = ">=1.83.0" },
1830
  { name = "ml-intern", extras = ["eval", "dev"], marker = "extra == 'all'" },
 
3609
  ]
3610
 
3611
  [[package]]
3612
+ name = "typer"
3613
+ version = "0.25.0"
3614
  source = { registry = "https://pypi.org/simple" }
3615
  dependencies = [
3616
+ { name = "annotated-doc" },
3617
  { name = "click" },
3618
+ { name = "rich" },
3619
+ { name = "shellingham" },
3620
  ]
3621
+ sdist = { url = "https://files.pythonhosted.org/packages/7b/27/ede8cec7596e0041ba7e7b80b47d132562f56ff454313a16f6084e555c9f/typer-0.25.0.tar.gz", hash = "sha256:123eaf9f19bb40fd268310e12a542c0c6b4fab9c98d9d23342a01ff95e3ce930", size = 120150, upload-time = "2026-04-26T08:46:14.767Z" }
3622
  wheels = [
3623
+ { url = "https://files.pythonhosted.org/packages/9a/72/193d4e586ec5a4db834a36bbeb47641a62f951f114ffd0fe5b1b46e8d56f/typer-0.25.0-py3-none-any.whl", hash = "sha256:ac01b48823d3db9a83c9e164338057eadbb1c9957a2a6b4eeb486669c560b5dc", size = 55993, upload-time = "2026-04-26T08:46:15.889Z" },
3624
  ]
3625
 
3626
  [[package]]