Skip to content

experimental/air: air list command (Jobs API, interactive UI) - #5793

Merged
riddhibhagwat-db merged 24 commits into
air-clifrom
air-integration-m1-2
Jul 1, 2026
Merged

experimental/air: air list command (Jobs API, interactive UI)#5793
riddhibhagwat-db merged 24 commits into
air-clifrom
air-integration-m1-2

Conversation

@riddhibhagwat-db

Copy link
Copy Markdown
Contributor

Changes

Adds air list — a browsable view of the caller's recent AIR training runs.

  • Data source: reads Jobs runs/list directly (expand_tasks) and filters to AIR runs. The current AIR task type is ai_runtime_task, which the typed Jobs SDK doesn't model, so the response is parsed raw for experiment, accelerators, status, timing, and user.
  • Interactive table: in a terminal air list renders an inline, navigable table (Bubble Tea + Lip Gloss + termenv) — ↑/↓ move a row, ←/→ page (20/screen), Enter opens the run's MLflow page, q quits. Status is colored by state; the MLflow column is a short clickable hyperlink.
  • Non-interactive: piped output, an explicit --limit, and empty results print the table once; -o json emits the air {v,ts,data} envelope. --limit defaults to 20.
  • Flags: --active, --all-users, and client-side --filter keys (experiment, accelerator_type, num_accelerators).
  • MLflow links are resolved per run from runs/get-output (ai_runtime_task_output, with the legacy gen_ai_compute_output as fallback) in text mode. This also fixes air get's MLflow column for ai_runtime_task runs.

Also adds cmdio.IsPagerSupported (stdin+stdout+stderr TTY) and promotes termenv to a direct dependency (NOTICE updated).

Why

The air CLI needs a run list on par with the Python air CLI and databricks jobs list-runs. It talks to the Jobs API directly rather than the AiWorkflowService, whose list RPC returns only run identifiers.

Tests

  • Unit: raw runs/list parse (incl. ai_runtime_task), row mapping, --filter matching, the TUI model (navigation, paging, 20-row cap, window scroll, quit), MLflow-id resolution (ai_runtime + legacy), and status/accelerator helpers.
  • Acceptance: acceptance/experimental/air/list (text + JSON, non-AIR filtered out, MLflow link resolved) plus help/get fixture updates.
  • Manual: verified against an e2-dogfood workspace (text, JSON, filters, --all-users, MLflow links).

This pull request and its description were written by Isaac.

Add the experimental `air` command group as the Go port surface for the
Python `air` CLI. Every subcommand (run, status, list, logs, cancel,
register-image) is registered as a stub that returns a not-implemented
error; the real implementations land in later milestones.

The package lives under experimental/air/cmd (imported as aircmd), matching
the layout of the other experimental features (aitools, genie, postgres);
cmd/experimental/ keeps only the dispatcher. TEST_PACKAGES in Taskfile.yml
gains ./experimental/air/... so the unit tests keep running after the move.

Includes unit tests for the command-tree wiring and the not-implemented
stubs, plus an acceptance test exercising the stubs end-to-end.

Co-authored-by: Isaac
Rename the run-details subcommand from `status` to `get`, matching the Python
air CLI's current `air get run` naming (it replaced `get status`). Renames the
file, constructor, command name, and updates the stub/help/unimplemented tests
and goldens accordingly.

Co-authored-by: Isaac
Rename the RUN_ID arg placeholder to JOB_RUN_ID across get/logs/cancel to
disambiguate it from other run identifiers. Hide the `logs --review` flag to
match the Python CLI (help=argparse.SUPPRESS), and add the `-i` shorthand for
`register-image --interactive-authenticate`.

Co-authored-by: Isaac
Implement the read-only run-details command (renamed from `status` to `get`).
It fetches a job run via the Jobs API and renders the run's status, start time,
duration, retries, experiment, accelerators, dashboard URL, MLflow deep-link,
and a foreach/sweep summary. Output is the air-style {v, ts, data} JSON envelope
under -o json, or a text view.

Renames the command-level identifiers (status -> get) while keeping the run's
"status" field/label. Adds format/mlflow/sweep/output helpers with unit tests
and an acceptance test, and drops `get` from the not-implemented stub coverage.

Co-authored-by: Isaac
The training-config block is command result data, but it was emitted via
cmdio.LogString, which targets stderr. Write it to cmd.OutOrStdout() instead so
it lands on stdout, matching the Python `air get`. Download/read failures stay
on stderr as warnings.

Co-authored-by: Isaac
`air get` derived Submitted and Duration from run-level start/end and truncated
milliseconds to seconds. Port Python's _reported_attempt_timing so a retried run
reports its latest attempt, and round to the nearest second to match Python's
round(). Drops the run-level RunDuration shortcut, which diverged on retries.

Co-authored-by: Isaac
mlflowURL resolved runs/get-output against Tasks[0], linking a retried run to its
stale first attempt. Use the last task (latest attempt) to match Python
(jobs_api_client.py:68).

Co-authored-by: Isaac
…N with Python

In -o json mode, error paths now emit the structured error envelope
({v, ts, error:{code, kind, message, retryable}}) and exit non-zero, matching
the Python air CLI's print_json_error instead of letting the framework print a
bare "Error: ..." string. Covers invalid RUN_ID, run-not-found, backend
failures, and client/auth failures (wrapped PreRunE).

Also align the success envelope with the Python CLI:
- dashboard_url: construct {host}/jobs/runs/{id}?o={workspace_id} (via
  CurrentWorkspaceID) instead of using the API's run_page_url
- started_at: datetime.isoformat() form ("+00:00" with microseconds), not
  RFC3339 "Z"
- duration_seconds: rounded half-to-even to match Python's round()
- use run-level start/end times for started_at and duration_seconds, dropping
  the last-attempt preference, which had no Python equivalent

Co-authored-by: Isaac
Revert the run-level timing change from the previous commit: started_at and
duration_seconds read from the last task's window again (reportedTiming),
matching the released Python `air` output, which reports the latest attempt.
The isoformat timestamp ("+00:00") and half-to-even rounding are kept.

Co-authored-by: Isaac
The runs/get-output call passed run_id via the query-param arg and a nil
request body, which this endpoint rejects with "expected a map", so the
MLflow link was never produced for completed runs. Pass run_id through the
request arg instead (the SDK serializes it to the query string for GET),
which sends a valid body and returns the gen_ai_compute_output run info.

Failed runs without MLflow output still yield no link: get-output 404s for
them, so mlflowURL returns nil as before.

Co-authored-by: Isaac
…output

Nest the run-status command under a `get` parent group so the command is
`air get run JOB_RUN_ID`, mirroring the Python CLI (the JOB_RUN_ID arg name
matches the sibling air commands and avoids confusion with the MLflow run id).

Align the text output with Python's `air get run`: lead with the dashboard
link (hyperlinked, falling back to the bare URL off a terminal) followed by a
gap, then the training config, then the status table. The table uses Python's
field order, "N/A" for empty cells, a "2006-01-02 15:04 UTC" Submitted
timestamp, and terminal hyperlinks on the Run ID, Experiment, and MLflow Run
cells (the MLflow Run cell shows the run's name from the MLflow REST API). The
JSON envelope is unchanged.

Also reformat the training-config YAML shown in text mode so multi-line
fields (e.g. command) render as block literals instead of escaped one-liners.

Co-authored-by: Isaac
Port `air list runs` from the Python air CLI: page through runs/list,
keep AIR (gen_ai_compute_task) workloads, filter by user and the
--filter keys (experiment glob, accelerator_type, num_accelerators),
and render either an aligned text table or the air JSON envelope.

MLflow links are fetched in parallel for the text table only, reusing
the get command's format helpers via a BaseRun->Run adapter.

Co-authored-by: Isaac
…tive UI

Move `air list` off the Jobs API onto the AiWorkflowService
ListTrainingWorkflows RPC (hand-rolled via client.Do, since the endpoint is
PUBLIC_UNDOCUMENTED), so the AIR filtering, creator scoping, and MLflow-ID
resolution live server-side.

Replace the static table with an inline, navigable table (Bubble Tea + Lip
Gloss + termenv): scroll with up/down, status colored by state, and a short
MLflow hyperlink that Enter opens in the browser. Piped output, an explicit
--limit, and empty results print the table once; `-o json` keeps the air
envelope. --limit now defaults to all.

Flatten the command to `air list` (no `runs` subcommand).

Add cmdio.IsPagerSupported (stdin+stdout+stderr TTY) and promote termenv to a
direct dependency.

Co-authored-by: Isaac
Add page-at-a-time navigation to the interactive runs table: left/right move
a screenful, home/end (g/G) jump to the ends. Line movement stays on up/down.

Co-authored-by: Isaac
The interactive table now shows at most 20 runs per page (still bounded by
terminal height), and left/right page by that amount.

Co-authored-by: Isaac
- Default --limit to 20 (was: all).
- Enter the interactive table only on a color TTY, so NO_COLOR falls back to
  the static table.
- Fetch full page batches when client-side --filter keys are set, since most
  rows may be dropped before reaching the limit.

Co-authored-by: Isaac
…amp repl

The acceptance [TIMESTAMP] replacement now also consumes the trailing "+00:00"
offset, so the committed started_at golden was stale after the rebase.

Co-authored-by: Isaac
ListTrainingWorkflows now returns only run identifiers, so `air list` reads Jobs
runs/list directly (expand_tasks) and parses ai_runtime_task — the current AIR
task type, which the typed SDK doesn't model — for experiment, accelerators,
status, timing, and user. Drops the AiWorkflowService dependency (aiworkflow.go).

MLflow links are resolved per run from runs/get-output (ai_runtime_task_output,
with the legacy gen_ai field as fallback), which also fixes `air get`'s MLflow
column for ai_runtime runs.

Co-authored-by: Isaac
@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Approval status: pending

/acceptance/experimental/air/ - needs approval

Files: acceptance/experimental/air/get/test.toml, acceptance/experimental/air/list/output.txt, acceptance/experimental/air/list/test.toml
Eligible: @apeforest, @bfontain, @lu-wang-dl, @panchalhp-db, @vinchenzo-db, @maggiewang-db, @ben-hansen-db, @pardis-beikzadeh-db

/experimental/air/ - needs approval

12 files changed
Eligible: @apeforest, @bfontain, @lu-wang-dl, @panchalhp-db, @vinchenzo-db, @maggiewang-db, @ben-hansen-db, @pardis-beikzadeh-db

Any maintainer (@andrewnester, @anton-107, @denik, @pietern, @shreyas-goenka, @simonfaltum, @renaudhartert-db) can approve all areas.
See OWNERS for ownership rules.

Resolve conflicts in favor of the Jobs-API `air list` (jobRun/joblist.go),
dropping the superseded AiWorkflowService list (aiworkflow.go). Keep air-cli's
other commands (run/cancel/compute/render) and its format helpers
(environment/maxRetries); rename the list's ai_runtime_task parse type to
jobAiRuntimeTask to avoid colliding with the submit path's aiRuntimeTask.

Co-authored-by: Isaac
@eng-dev-ecosystem-bot

Copy link
Copy Markdown
Collaborator

Integration test report

Commit: 1c6e95e

Run: 28538915204

Env 💚​RECOVERED 🙈​SKIP ✅​pass 🙈​skip Time
💚​ aws linux 10 13 261 1016 7:16
💚​ aws windows 10 13 263 1014 9:57
💚​ aws-ucws linux 10 13 357 930 8:01
💚​ aws-ucws windows 10 13 359 928 10:46
💚​ azure linux 4 15 264 1014 8:19
💚​ azure windows 4 15 266 1012 9:25
💚​ azure-ucws linux 4 15 362 926 8:00
💚​ azure-ucws windows 4 15 364 924 10:32
💚​ gcp linux 4 15 260 1017 7:18
💚​ gcp windows 4 15 262 1015 11:12
23 interesting tests: 13 SKIP, 10 RECOVERED
Test Name aws linux aws windows aws-ucws linux aws-ucws windows azure linux azure windows azure-ucws linux azure-ucws windows gcp linux gcp windows
💚​ TestAccept 💚​R 💚​R 💚​R 💚​R 💚​R 💚​R 💚​R 💚​R 💚​R 💚​R
🙈​ TestAccept/bundle/invariant/no_drift 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S
🙈​ TestAccept/bundle/resources/permissions 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S
💚​ TestAccept/bundle/resources/permissions/jobs/destroy_without_mgmtperms/with_permissions 💚​R 💚​R 💚​R 💚​R 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S
💚​ TestAccept/bundle/resources/permissions/jobs/destroy_without_mgmtperms/with_permissions/DATABRICKS_BUNDLE_ENGINE=direct 💚​R 💚​R 💚​R 💚​R
💚​ TestAccept/bundle/resources/permissions/jobs/destroy_without_mgmtperms/with_permissions/DATABRICKS_BUNDLE_ENGINE=terraform 💚​R 💚​R 💚​R 💚​R
💚​ TestAccept/bundle/resources/permissions/jobs/destroy_without_mgmtperms/without_permissions 💚​R 💚​R 💚​R 💚​R 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S
💚​ TestAccept/bundle/resources/permissions/jobs/destroy_without_mgmtperms/without_permissions/DATABRICKS_BUNDLE_ENGINE=direct 💚​R 💚​R 💚​R 💚​R
💚​ TestAccept/bundle/resources/permissions/jobs/destroy_without_mgmtperms/without_permissions/DATABRICKS_BUNDLE_ENGINE=terraform 💚​R 💚​R 💚​R 💚​R
🙈​ TestAccept/bundle/resources/postgres_branches/basic 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S
🙈​ TestAccept/bundle/resources/postgres_branches/recreate 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S
🙈​ TestAccept/bundle/resources/postgres_branches/replace_existing 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S
🙈​ TestAccept/bundle/resources/postgres_branches/update_protected 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S
🙈​ TestAccept/bundle/resources/postgres_branches/without_branch_id 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S
🙈​ TestAccept/bundle/resources/postgres_endpoints/basic 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S
🙈​ TestAccept/bundle/resources/postgres_projects/update_display_name 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S
🙈​ TestAccept/bundle/resources/synced_database_tables/basic 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S
🙈​ TestAccept/bundle/resources/vector_search_endpoints/drift/recreated_same_name 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S
🙈​ TestAccept/bundle/resources/vector_search_indexes/recreate/embedding_dimension 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S
🙈​ TestAccept/ssh/connection 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S
💚​ TestFetchRepositoryInfoAPI_FromRepo 💚​R 💚​R 💚​R 💚​R 💚​R 💚​R 💚​R 💚​R 💚​R 💚​R
💚​ TestFetchRepositoryInfoAPI_FromRepo/root 💚​R 💚​R 💚​R 💚​R 💚​R 💚​R 💚​R 💚​R 💚​R 💚​R
💚​ TestFetchRepositoryInfoAPI_FromRepo/subdir 💚​R 💚​R 💚​R 💚​R 💚​R 💚​R 💚​R 💚​R 💚​R 💚​R
Top 30 slowest tests (at least 2 minutes):
duration env testname
5:04 gcp windows TestAccept
4:53 aws windows TestAccept
4:52 azure-ucws windows TestAccept
4:48 aws-ucws windows TestAccept
4:47 azure windows TestAccept
4:12 gcp windows TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=terraform
3:59 gcp linux TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=direct
3:57 gcp windows TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=direct
3:45 gcp linux TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=terraform
3:27 aws-ucws windows TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=terraform
3:23 aws-ucws windows TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=direct
3:22 aws-ucws linux TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=direct
3:19 aws linux TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=terraform
3:16 azure linux TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=direct
3:13 azure linux TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=terraform
3:07 azure windows TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=terraform
3:04 aws windows TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=terraform
3:03 aws windows TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=direct
2:58 azure-ucws windows TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=direct
2:53 azure linux TestAccept
2:50 gcp linux TestAccept
2:49 azure-ucws windows TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=terraform
2:49 aws linux TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=direct
2:47 azure windows TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=direct
2:45 aws linux TestAccept
2:40 azure-ucws linux TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=terraform
2:39 azure-ucws linux TestAccept
2:39 aws-ucws linux TestAccept
2:38 aws-ucws linux TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=terraform
2:26 azure-ucws linux TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=direct

@riddhibhagwat-db
riddhibhagwat-db merged commit 8b3f0d5 into air-cli Jul 1, 2026
21 checks passed
@riddhibhagwat-db
riddhibhagwat-db deleted the air-integration-m1-2 branch July 1, 2026 19:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants