aitools: categorize install errors - #6482
Conversation
Approval status: pending
|
583618b to
5dbbee3
Compare
1bec626 to
475a007
Compare
Review — multi-reviewer passReviewed the incremental diff (against the stacked parent 🔴 Blocking — per-agent category serializes under the wrong JSON key
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 🟡 Nice to have
⚪ Nits
|
anton-107
left a comment
There was a problem hiding this comment.
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 ReasonNoPlugin → UNCATEGORIZED is unreachable since plugin-less agents route to deliverySkills.
| // user-authored text. | ||
| type AitoolsAgentResult struct { | ||
| Agent AitoolsAgentType `json:"agent"` | ||
| ErrorCategory AitoolsErrorCategory `json:"errorCategory"` |
There was a problem hiding this comment.
Blocking (naming): this is tagged errorCategory (camelCase) while every other field in libs/telemetry/protos is snake_case — including error_category 18 lines below in this same file, plus compute_type, resource_job_count, aitools_install_event, etc.
These structs are hand-maintained mirrors of the lumberjack proto, so this nested per-agent field is the one whose wire name diverges from the proto field name. protojson does accept lowerCamelCase, so it probably parses today, but that's a silent dependency that breaks under a strict/snake-only decoder.
| ErrorCategory AitoolsErrorCategory `json:"errorCategory"` | |
| ErrorCategory AitoolsErrorCategory `json:"error_category"` |
| // is the local-only message; ErrorCategory is the classification telemetry | ||
| // also records. | ||
| Error string `json:"error,omitempty"` | ||
| ErrorCategory string `json:"errorCategory,omitempty"` |
There was a problem hiding this comment.
Blocking (naming): errorCategory here (and on agentResultJSON below) is a public output contract for aitools install --output json, so it can't be renamed after release.
The CLI's JSON output convention is snake_case throughout — cmd/fs/ls.go (is_directory, last_modified), cmd/auth/profiles.go (account_id, auth_type), cmd/bundle/debug/fetch_repository_info.go (worktree_root). The existing keys here (scope, agents, name, delivery, status, message) are all single words, so this PR is the first to set the multi-word precedent.
| ErrorCategory string `json:"errorCategory,omitempty"` | |
| ErrorCategory string `json:"error_category,omitempty"` |
| Name string `json:"name"` | ||
| Delivery string `json:"delivery"` | ||
| Status string `json:"status"` | ||
| ErrorCategory string `json:"errorCategory,omitempty"` |
There was a problem hiding this comment.
Same rename here, for the per-agent entry.
| ErrorCategory string `json:"errorCategory,omitempty"` | |
| ErrorCategory string `json:"error_category,omitempty"` |
| for _, o := range outcomes { | ||
| // A successful agent leaves errorCategory at its zero value ("", not the | ||
| // TYPE_UNSPECIFIED sentinel), so key on emptiness to drop it here. | ||
| if o.agent == nil || o.errorCategory == "" { |
There was a problem hiding this comment.
Non-blocking, two separate notes on this line.
1. The o.agent == nil guard disagrees with the other consumer of the same slice. buildInstallOutput (cmd/aitools/install.go:601) dereferences o.agent.Name with no guard. So either a nil agent is reachable — in which case --output json panics on exactly the input the telemetry path tolerates — or it isn't, and the guard plus its test (telemetry_test.go, "A nil agent is skipped defensively") are dead code that CLAUDE.md's "no defensive nil checks for values the caller always provides" rule asks you to drop. Either way the two functions should agree.
2. Keying success off errorCategory == "" is a trap for future code. The comment correctly notes that "" is the Go zero value and not the AitoolsErrorCategoryUnspecified sentinel — but since the field is typed protos.AitoolsErrorCategory, it's natural for someone to later write errorCategory: protos.AitoolsErrorCategoryUnspecified for a successful agent, which would emit a bogus agent_results entry claiming that agent failed with TYPE_UNSPECIFIED. Keying on o.status != outcomeInstalled (or checking both "" and Unspecified) removes the trap.
| ) | ||
|
|
||
| func (e *SkillError) Error() string { | ||
| return fmt.Sprintf("skill %q %s", e.Skill, e.Detail) |
There was a problem hiding this comment.
Non-blocking: when Detail is empty this renders as skill "databricks" — trailing space, no reason given. Both current construction sites set Detail, but the new test already builds &installer.SkillError{Skill: "databricks", Reason: "some-future-reason"} without one, which is exactly the shape a future reason will take; that message would then leak into --output json's error field and the text Error: line.
Falling back to Reason when Detail == "" (the way BlockedError.Error() already branches on it) keeps it self-describing.
| // ErrorCategory is the top-level command outcome: the category of the error | ||
| // that failed the run, or Unspecified when the command succeeded. It captures | ||
| // failures that have no per-agent entry (e.g. a skills-group install failure). | ||
| ErrorCategory AitoolsErrorCategory `json:"error_category,omitempty"` |
There was a problem hiding this comment.
Non-blocking: omitempty never fires here, since the success value is AitoolsErrorCategoryUnspecified = "TYPE_UNSPECIFIED", a non-empty string — so every successful install ships "error_category":"TYPE_UNSPECIFIED". Harmless on the wire, but the tag implies an omission that can't happen. The precedent for an always-populated category field, SshTunnelEvent.ErrorCategory (libs/telemetry/protos/ssh_tunnel.go:116), deliberately omits omitempty.
5dbbee3 to
0f9f04a
Compare
475a007 to
7c97955
Compare
0f9f04a to
af4bb2a
Compare
929fe31 to
4882a9b
Compare
af4bb2a to
047ee54
Compare
4882a9b to
0c9f703
Compare
Integration test reportCommit: 49636b6
Top 2 slowest tests (at least 2 minutes):
|
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>
5a6cd9f to
49636b6
Compare
Stacked on #6481
Changes
Categorize
aitools installerrors, and emit those in telemetry and JSON outputWhy
To better understand why installations failed
Tests
Added unit tests