Skip to content

AIR CLI Integration: air list Functionality & UI (Interfacing with Training Service) - #5684

Merged
riddhibhagwat-db merged 22 commits into
air-clifrom
air-integration-m1-2
Jun 23, 2026
Merged

AIR CLI Integration: air list Functionality & UI (Interfacing with Training Service) #5684
riddhibhagwat-db merged 22 commits into
air-clifrom
air-integration-m1-2

Conversation

@riddhibhagwat-db

Copy link
Copy Markdown
Contributor

Changes

Add air list as a browsable view of the caller's recent AIR training runs.

  • Data source: the AiWorkflowService.ListTrainingWorkflows RPC (GET /api/2.0/ai-training/workflows), called directly via client.Do since the endpoint is PUBLIC_UNDOCUMENTED and not modeled by the SDK. The server does the AIR filtering, creator scoping, MLflow-ID resolution, and pagination, so no Jobs-API logic lives in the CLI.
  • Interactive table: in a terminal air list renders an inline, navigable table (Bubble Tea + Lip Gloss + termenv): ↑/↓ move a row, ←/→ page (20 rows/page), Enter opens the run's MLflow page, q quits. Status is colored by state and 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 unchanged.
  • Flags: --limit (default: all), --active, --all-users, and client-side --filter keys (experiment, accelerator_type, num_accelerators). Gateway timeouts (e.g. HTTP 504 on --all-users) return an actionable message.
  • Adds cmdio.IsPagerSupported; promotes termenv to a direct dependency

Why

The ai-training service now owns the AIR-specific run logic server-side, so air list should call its RPC rather than reimplementing run discovery against the Jobs API. The interactive table gives a browsable run list on par with the Python air CLI and databricks jobs list-runs.

Tests

  • Unit: RPC transport, TrainingWorkflow→row mapping, --filter matching, status/accelerator/timestamp helpers, and the TUI model (navigation, paging, 20-row page cap, window scroll, quit, static render).
  • Acceptance: acceptance/experimental/air/list (text + JSON) plus help updates; unimplemented no longer covers air list

Manual verification output:
Screenshot 2026-06-22 at 11 52 41 AM

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
@riddhibhagwat-db
riddhibhagwat-db changed the base branch from main to air-cli June 22, 2026 18:53
@github-actions

github-actions Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Approval status: pending

/acceptance/experimental/air/ - needs approval

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

/experimental/air/ - needs approval

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

/libs/cmdio/ - needs approval

Files: libs/cmdio/io.go
Suggested: @simonfaltum
Also eligible: @renaudhartert-db, @hectorcast-db, @parthban-db, @tanmay-db, @Divyansh-db, @tejaskochar-db, @mihaimitrea-db, @chrisst, @rauchy

General files (require maintainer)

Files: NOTICE, go.mod
Based on git history:

  • @pietern -- recent work in ./, libs/cmdio/

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

@eng-dev-ecosystem-bot

eng-dev-ecosystem-bot commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator

Integration test report

Commit: c7fdd86

Run: 28059872418

Env 🟨​KNOWN 💚​RECOVERED 🙈​SKIP ✅​pass 🙈​skip Time
🟨​ aws linux 7 13 264 1015 10:16
🟨​ aws windows 7 13 266 1013 11:27
💚​ aws-ucws linux 7 13 360 929 6:14
💚​ aws-ucws windows 7 13 362 927 9:00
💚​ azure linux 1 15 267 1013 6:21
💚​ azure windows 1 15 269 1011 7:48
💚​ azure-ucws linux 1 15 365 925 13:39
💚​ azure-ucws windows 1 15 367 923 8:51
💚​ gcp linux 1 15 263 1016 6:36
💚​ gcp windows 1 15 265 1014 8:18
20 interesting tests: 13 SKIP, 7 KNOWN
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 🟨​K 🟨​K 💚​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 🟨​K 🟨​K 💚​R 💚​R 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S
🟨​ TestAccept/bundle/resources/permissions/jobs/destroy_without_mgmtperms/with_permissions/DATABRICKS_BUNDLE_ENGINE=direct 🟨​K 🟨​K 💚​R 💚​R
🟨​ TestAccept/bundle/resources/permissions/jobs/destroy_without_mgmtperms/with_permissions/DATABRICKS_BUNDLE_ENGINE=terraform 🟨​K 🟨​K 💚​R 💚​R
🟨​ TestAccept/bundle/resources/permissions/jobs/destroy_without_mgmtperms/without_permissions 🟨​K 🟨​K 💚​R 💚​R 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S
🟨​ TestAccept/bundle/resources/permissions/jobs/destroy_without_mgmtperms/without_permissions/DATABRICKS_BUNDLE_ENGINE=direct 🟨​K 🟨​K 💚​R 💚​R
🟨​ TestAccept/bundle/resources/permissions/jobs/destroy_without_mgmtperms/without_permissions/DATABRICKS_BUNDLE_ENGINE=terraform 🟨​K 🟨​K 💚​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
Top 28 slowest tests (at least 2 minutes):
duration env testname
6:55 aws linux TestSecretsPutSecretStringValue
6:47 azure-ucws linux TestSQLExecScalar
4:18 gcp linux TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=direct
4:17 gcp windows TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=terraform
4:11 gcp linux TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=terraform
4:01 gcp windows TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=direct
3:34 aws windows TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=direct
3:27 gcp windows TestAccept
3:23 azure linux TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=terraform
3:19 aws-ucws linux TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=terraform
3:16 aws-ucws windows TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=terraform
3:15 aws-ucws windows TestAccept
3:13 azure linux TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=direct
3:12 azure-ucws windows TestAccept
3:09 azure windows TestAccept
3:01 azure windows TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=terraform
2:59 aws linux TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=terraform
2:58 azure-ucws linux TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=terraform
2:57 azure-ucws windows TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=terraform
2:57 aws-ucws windows TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=direct
2:52 aws windows TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=terraform
2:38 aws-ucws linux TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=direct
2:37 azure windows TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=direct
2:34 aws linux TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=direct
2:33 gcp linux TestSecretsPutSecretStringValue
2:33 azure-ucws windows TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=direct
2:25 azure-ucws linux TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=direct
2:18 aws windows TestSecretsPutSecretStringValue

Comment thread experimental/air/cmd/list.go Outdated
Comment thread experimental/air/cmd/list_tui.go
Comment thread experimental/air/cmd/list.go
- 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
@riddhibhagwat-db
riddhibhagwat-db merged commit bd3f934 into air-cli Jun 23, 2026
28 checks passed
@riddhibhagwat-db
riddhibhagwat-db deleted the air-integration-m1-2 branch June 23, 2026 22:58
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.

3 participants