Skip to content

aitools: categorize install errors - #6482

Open
rclarey wants to merge 9 commits into
mainfrom
aitools-install-error-categories-stacked
Open

aitools: categorize install errors#6482
rclarey wants to merge 9 commits into
mainfrom
aitools-install-error-categories-stacked

Conversation

@rclarey

@rclarey rclarey commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Stacked on #6481

Changes

Categorize aitools install errors, and emit those in telemetry and JSON output

Why

To better understand why installations failed

Tests

Added unit tests

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Approval status: pending

/cmd/aitools/ - needs approval

6 files changed
Suggested: @lennartkats-db
Also eligible: @parthban-db, @rugpanov, @misha-db, @anton-107, @fjakobs, @Shridhad, @atilafassina, @keugenek, @igrekun, @pkosiec, @MarioCadenas, @pffigueiredo, @ditadi, @calvarjorge

/libs/aitools/ - needs approval

Files: libs/aitools/installer/errors.go, libs/aitools/installer/installer.go
Suggested: @lennartkats-db
Also eligible: @parthban-db, @rugpanov, @misha-db, @anton-107, @fjakobs, @Shridhad, @atilafassina, @keugenek, @igrekun, @pkosiec, @MarioCadenas, @pffigueiredo, @ditadi, @calvarjorge

/libs/telemetry/ - needs approval

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

General files (require maintainer)

13 files changed
Based on git history:

  • @simonfaltum -- recent work in cmd/aitools/, libs/aitools/installer/

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

@rclarey
rclarey force-pushed the aitools-install-output-json branch from 583618b to 5dbbee3 Compare September 2, 2026 11:10
@rclarey
rclarey force-pushed the aitools-install-error-categories-stacked branch from 1bec626 to 475a007 Compare September 2, 2026 11:10
@rugpanov

rugpanov commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Review — multi-reviewer pass

Reviewed the incremental diff (against the stacked parent aitools-install-output-json) with several independent reviewers and verified each finding against the code. Design and correctness are sound overall, and CI (including integration tests) is green. One blocking wire-format bug, plus a few low-risk cleanups.

🔴 Blocking — per-agent category serializes under the wrong JSON key

libs/telemetry/protos/aitools_install.go:53

type AitoolsAgentResult struct {
	Agent         AitoolsAgentType     `json:"agent"`
	ErrorCategory AitoolsErrorCategory `json:"errorCategory"`   // should be "error_category"
}

This is the only camelCase JSON tag in the whole libs/telemetry/protos package (111 other tags are snake_case), and it disagrees with its own sibling field on the parent event (error_category, line 71) and the existing precedents in ssh_tunnel.go / bundle_config_remote_sync.go. The marshaled payload becomes agent_results:[{"agent":"CODEX","errorCategory":...}], so the per-agent category won't map to the lumberjack proto's error_category field and is likely dropped on ingestion — which is exactly the per-agent signal this PR adds. Fix: json:"error_category".

🟡 Nice to have

  • A sibling specific-skill failure is left uncategorizedlibs/aitools/installer/installer.go:487. The "experimental skill; use --experimental" specific-failure still returns a plain fmt.Errorf, so it classifies as UNCATEGORIZED, while the two adjacent failures in the same isSpecific branch (not-found :475, version-incompatible :495) were converted to *SkillError. Consider a category (e.g. EXPERIMENTAL_SKILL) or making it a SkillError for consistency.

  • Defensive nil checkcmd/aitools/telemetry.go:58. o.agent == nil in agentResultsField guards against a state that can't occur (every outcome is built from a non-nil plan agent). Per the repo convention on unjustified nil checks, consider removing it (and the synthetic nil test case) or adding a comment on why the invariant might break. (It does mirror the pre-existing pattern in agentsField, so it's at least locally consistent.)

  • No test asserts the telemetry wire format — nothing marshals AitoolsInstallEvent/AitoolsAgentResult to JSON and checks the keys. A serialization assertion would have caught the blocking finding above.

  • UNSUPPORTED_SCOPE on exit-0 skipscmd/aitools/install.go:391,400,555. An ordinary scope-incompatible skip (status skipped, exit 0) still lands in agent_results with UNSUPPORTED_SCOPE. Looks intentional per the doc comment, but conflating expected skips with error rows may inflate error analytics — worth confirming the intended semantics.

⚪ Nits

  • cmd/aitools/install.go:606if o.errorCategory != "" { … } is redundant given the omitempty tag, and inconsistent with the unconditional top-level set just below it.
  • cmd/aitools/install.go:64 — the field skipError holds a category, not an error; mildly misleading next to errorCategory.
  • libs/aitools/installer/errors.go:25SkillError.Error() produces a trailing space when Detail is empty (only reachable from a unit test today).

@anton-107 anton-107 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice change — the classification logic itself looks right to me. I built the branch and ran go test ./cmd/aitools/... ./libs/aitools/... ./libs/telemetry/..., go vet and gofmt: all clean. I couldn't find a correctness bug; the defer closure capture of outcomes/runErr, the topLevelFailure gate, and the SkillError message reconstruction (byte-identical to the strings it replaces) all check out.

Requesting changes on naming only — two new errorCategory keys are camelCase where the surrounding code is snake_case, and one of them is a public output contract for aitools install --output json that can't be renamed after release. Details inline.

The remaining inline comments are non-blocking robustness/consistency notes; take or leave them as you see fit.

Two things I checked and cleared: both deliverySkip branches in planItemFor genuinely mean unsupported scope (mapAgentScope's only ok == false return is the project-scope case), and ReasonNoPluginUNCATEGORIZED is unreachable since plugin-less agents route to deliverySkills.

Comment thread libs/telemetry/protos/aitools_install.go Outdated
Comment thread cmd/aitools/install.go Outdated
Comment thread cmd/aitools/install.go Outdated
Comment thread cmd/aitools/telemetry.go Outdated
Comment thread libs/aitools/installer/errors.go Outdated
Comment thread libs/telemetry/protos/aitools_install.go Outdated
@rclarey
rclarey force-pushed the aitools-install-output-json branch from 5dbbee3 to 0f9f04a Compare September 3, 2026 12:21
@rclarey
rclarey force-pushed the aitools-install-error-categories-stacked branch from 475a007 to 7c97955 Compare September 3, 2026 12:21
@rclarey
rclarey force-pushed the aitools-install-output-json branch from 0f9f04a to af4bb2a Compare September 3, 2026 13:46
@rclarey
rclarey force-pushed the aitools-install-error-categories-stacked branch from 7c97955 to 929fe31 Compare September 3, 2026 13:46
Add JSON output to `aitools install`, driven entirely by flags so the
run is fully non-interactive: require --scope and --agents (erroring and
naming the missing flags otherwise) so no scope prompt, agent picker, or
confirm is shown. executePlan now returns a per-agent outcome (name,
delivery, status, message) that the JSON payload lists. A top-level
failure with no per-agent entry (e.g. a skills-group install failure) is
surfaced in a top-level "error" field; per-agent failures stay in their
agent entry and are not duplicated there (executePlan wraps them so the
two are distinguishable). Once the JSON result is rendered, silence
cobra's text "Error:"/usage output so a failure is not reported twice;
the non-zero exit still comes from returning the run error. Share the
indented-JSON encoder between install and list as renderJSON.

Co-authored-by: Isaac <no-reply@databricks.com>
@rclarey
rclarey force-pushed the aitools-install-error-categories-stacked branch from 929fe31 to 4882a9b Compare September 3, 2026 14:04
@rclarey
rclarey force-pushed the aitools-install-output-json branch from af4bb2a to 047ee54 Compare September 3, 2026 14:04
The changelog validator now requires each fragment to be a single line
starting with a `* ` bullet and ending with a period before the trailing
PR link group. Reformat the install --output json fragment accordingly.

Co-authored-by: Isaac <no-reply@databricks.com>
@rclarey
rclarey force-pushed the aitools-install-error-categories-stacked branch from 4882a9b to 0c9f703 Compare September 4, 2026 10:27
@eng-dev-ecosystem-bot

eng-dev-ecosystem-bot commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator

Integration test report

Commit: 501eaf7

Run: 34126654511

Env 🔄​flaky 💚​RECOVERED ✅​pass 🙈​skip Time
💚​ aws linux 1 275 18 8:20
🔄​ aws windows 1 1 276 16 8:01
💚​ azure linux 1 274 18 10:26
💚​ azure windows 1 276 16 7:08
🔄​ gcp linux 2 1 273 18 14:37
🔄​ gcp windows 3 275 16 10:31
6 interesting tests: 6 flaky
Test Name aws linux aws windows azure linux azure windows gcp linux gcp windows
🔄​ TestAccept 💚​R 💚​R 💚​R 💚​R 💚​R 🔄​f
🔄​ TestSyncIncrementalFileOverwritesFolder ✅​p ✅​p ✅​p ✅​p 🔄​f ✅​p
🔄​ TestSyncIncrementalFileSync ✅​p ✅​p ✅​p ✅​p ✅​p 🔄​f
🔄​ TestSyncIncrementalSyncPythonNotebookToFile ✅​p ✅​p ✅​p ✅​p 🔄​f ✅​p
🔄​ TestSyncNestedFolderDoesntFailOnNonEmptyDirectory ✅​p 🔄​f ✅​p ✅​p ✅​p ✅​p
🔄​ TestSyncNestedFolderSync ✅​p ✅​p ✅​p ✅​p ✅​p 🔄​f
Top 12 slowest tests (at least 2 minutes):
duration env testname
4:07 azure windows TestAccept
3:58 aws windows TestAccept
3:54 gcp linux TestFilerWorkspaceFilesExtensionsReadDir
3:52 gcp linux TestAccept
3:47 azure linux TestAccept
3:44 aws linux TestAccept
3:34 gcp windows TestFilerWorkspaceFilesExtensionsStat
3:08 gcp windows TestAccept
2:30 aws windows TestSyncIncrementalFileSync
2:12 aws windows TestFilerWorkspaceFilesExtensionsDelete
2:02 gcp windows TestFilerReadWrite/workspace_files
2:00 azure windows TestFilerWorkspaceFilesExtensionsReadDir

--output json is meant to emit only the structured JSON document, but the
installer still wrote progress lines ("Using skills version", "Fetching
skills manifest...", "Installed N skills.") to stderr, so a consumer saw
non-JSON interleaved with the result. Mark the context quiet in JSON mode
and route those library messages through cmdio.LogProgress, which respects
it. Text mode is unaffected. Update the acceptance test to use the
non-deprecated `aitools install` and assert only JSON is emitted.

Co-authored-by: Isaac <no-reply@databricks.com>
@rclarey
rclarey force-pushed the aitools-install-error-categories-stacked branch from 5a6cd9f to 49636b6 Compare September 4, 2026 14:34
rclarey and others added 5 commits September 7, 2026 10:48
Classify why an `aitools install` run, or one agent within it, failed
into a stable AitoolsErrorCategory, so install failures can be aggregated
in telemetry without sending any user-authored error text. Introduce
SkillError alongside the existing BlockedError, map both to categories via
classifyInstallError, and record the per-agent categories on the install
event. Surface the per-agent category in the `--output json` result too.

The top-level errorCategory (telemetry and JSON) is set only for a
failure with no per-agent entry; a per-agent failure keeps its category
in its own entry and leaves the top-level category Unspecified, so it is
never counted twice.

Co-authored-by: Isaac <no-reply@databricks.com>
- Rename the JSON/telemetry field errorCategory -> error_category
  (snake_case), matching the CLI's --output json convention and the rest
  of libs/telemetry/protos. Update the acceptance golden accordingly.
- Drop omitempty on AitoolsInstallEvent.ErrorCategory: it is always
  populated (Unspecified on success), so the tag never fired.
- agentResultsField: key on status == outcomeInstalled instead of
  errorCategory == "", and drop the dead o.agent == nil guard (agents
  always come from the validated registry, matching buildInstallOutput).
- SkillError.Error() falls back to Reason when Detail is empty so the
  message stays self-describing.

Co-authored-by: Isaac <no-reply@databricks.com>
Use the non-deprecated `aitools install` command. With progress now
silenced in JSON mode (see the --output json branch), stdout carries only
the JSON document, so the golden no longer has non-JSON text before it.

Co-authored-by: Isaac <no-reply@databricks.com>
Cover the --output json path where a failure is reported per agent rather
than at the top level:
- install-output-json-agent-error: claude-code installs while cursor is
  skipped with an UNSUPPORTED_SCOPE category in its own agents[] entry, so
  the array mixes a success with a per-agent error and the top-level
  error_category stays unset.
- install-output-json-agents-skipped: every named agent (cursor, codex) is
  skipped for scope, giving multiple per-agent categories with no successful
  install and no top-level error.

Both exit non-zero because the agents were named explicitly.

Co-authored-by: Isaac <no-reply@databricks.com>
@rclarey
rclarey force-pushed the aitools-install-error-categories-stacked branch from 2d006b5 to c0840bd Compare September 7, 2026 08:54
@rclarey
rclarey requested a review from anton-107 September 7, 2026 08:55
Base automatically changed from aitools-install-output-json to main September 7, 2026 12:42
@rclarey
rclarey requested a review from rugpanov September 7, 2026 13:14

@anton-107 anton-107 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks — all six threads from my previous pass are properly addressed, and the rename went further than the structs (acceptance golden, the script's explanatory comment, and the changelog fragment are all updated; git grep 'json:"[a-z]*[A-Z]' over cmd/aitools, libs/aitools, libs/telemetry/protos is empty). Both blocking items are resolved. I'm leaving my requested-changes in place for now only pending the installer.go:487 question at the bottom — everything else here is non-blocking.

I re-verified rather than taking the resolutions on trust: gofmt and go vet clean, go test ./cmd/aitools/... ./libs/aitools/... ./libs/telemetry/... green, and go test ./acceptance -run TestAccept/experimental/aitools green, so the goldens genuinely match. Two details I liked: you took the drop the guard fork on the nil check (so agentResultsField and buildInstallOutput now agree), and status == outcomeInstalled is an exact success test rather than an approximation, since outcomeStatus has exactly three values.

Two small leftovers from my point on agentResultsField, inline. Both non-blocking.

One request before merge: could you take another look at Grigory's review comment? Its blocking item was the same camelCase tag, so that's covered, but four of its items got no change and no reply, and one of them looks substantive to me:

  • libs/aitools/installer/installer.go:487"skill %q is experimental; use --experimental to install" is still a plain fmt.Errorf, so classifyInstallError lands it in UNCATEGORIZED, while the two sibling failures in the same isSpecific branch were converted to *SkillError. That's a classification gap in exactly the signal this PR adds — a real, reachable user error that will show up as uncategorized noise. Worth a SkillError reason, or an explicit note that uncategorized is intended for it.
  • cmd/aitools/install.go:65skipError holds a protos.AitoolsErrorCategory, not an error; mildly confusing sitting next to errorCategory.
  • The UNSUPPORTED_SCOPE-on-exit-0 skip semantics question. Your two new acceptance tests document the explicit-agents (exit 1) case nicely, but the auto-detected-agents case still records an error category on a successful run — fine if intended, just never answered.
  • The missing wire-format test, which I've also flagged inline.

Happy for all of these to be follow-ups if you'd rather keep this PR tight — I'd just like the installer.go:487 one either fixed or explicitly deferred rather than dropped, and then I'll clear the block.

Comment thread cmd/aitools/install.go
Status: string(o.status),
Message: o.message,
}
if o.errorCategory != "" {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking, follow-up to my earlier note on agentResultsField: the trap moved rather than disappeared. Telemetry now keys success off status == outcomeInstalled, but this side still keys off errorCategory != "", so the two consumers of the same slice disagree again — just in the opposite direction from before. A future successful outcome carrying AitoolsErrorCategoryUnspecified would be correctly dropped from agent_results and still emit "error_category": "TYPE_UNSPECIFIED" here, on an agent whose status is installed.

Separately, the if is redundant as written: string("") is "" and the tag is omitempty, so an unconditional assignment is byte-identical on the wire. Keying on o.status != outcomeInstalled (matching telemetry.go) or just dropping the branch both work; the former keeps the two functions aligned.

// user-authored text.
type AitoolsAgentResult struct {
Agent AitoolsAgentType `json:"agent"`
ErrorCategory AitoolsErrorCategory `json:"error_category"`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking: with the tag fixed, nothing guards it. No test marshals AitoolsInstallEvent/AitoolsAgentResult and asserts the key names, so the next hand-edit to these structs can reintroduce exactly the bug we just fixed — and it fails silently, on the ingestion side, where nobody sees it.

libs/telemetry/protos/ssh_tunnel_test.go is the precedent in this package: it marshals the event and compares against a literal JSON string. A ~10-line equivalent covering an event with one agent_results entry would pin both error_category fields. Grigory raised this too.

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.

4 participants