fix(agents): decode camelCase API responses, draft the suggested action - #195
Merged
Merged
Conversation
The Agents API serializes its domain types straight to JSON, and coast#515
("chore: camelCase findings domain types") renamed those types from
snake_case. Our structs still declared snake_case tags, so every multi-word
response field silently decoded to its zero value: no savings, no
timestamps, no actionDescription, no suggestedAction. Single-word fields
(id, title, status, effort) kept working, which is why nothing looked
obviously broken.
The visible symptom was `preview_fix` returning a 500 for FinOps tagging
findings. Agents deliberately downgrades a tagging fix to `create_ticket`
when the resources aren't managed by Terraform, but suggestedAction never
reached the caller, so preview-fix fell back to its hardcoded `open_pr` —
and generate-action 500s for a task with no repo to name.
- Retag Finding / Task / Action / FindingEvent / FindingTaskEvent to
camelCase, and move request bodies over too (Agents dual-accepts both
casings today, and its own comments call our snake_case a compat shim).
Response fields that are genuinely snake_case server-side stay put —
action_id and the report endpoint's learning_id / *_action_ids are
hand-built route literals, not serialized domain types.
- Drop has_running_actions / integration_id, which have no server
counterpart, and add lifecycleState / remediationState / prUrl, which do.
- Fix findings-list paging: the endpoint is offset-paginated (page /
per_page, returning {items, pagination}), so `--cursor` was a silent
no-op that pinned every caller to the first page. It's now `--page`.
- Default `preview-fix` to the task's own suggestedAction. An explicit
--type still wins and skips the lookup; a failed lookup or a task with no
suggestion falls back to open_pr; a `manual` suggestion errors with what
to do instead. The --type flag defaulted to "open_pr" at the cobra layer,
which would have defeated this.
- Honor the drafted type in `create-fix`: the {type, config} envelope's type
was discarded, so piping a ticket draft through created an open_pr with a
ticket config. This started to matter once tagging tasks actually drafted
tickets.
Pin the wire casing in a client test so this can't rot silently again.
brandongregoryscott
approved these changes
Jul 28, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
preview_fixhas been returning500 {"error":"failed to generate action config"}for FinOps tagging findings. Reproduced on the demo org:Root cause is a wire-contract drift in our Agents client. The API serializes its domain types straight to JSON (
c.json<FindingDetail>(finding)), and the Agents API renamed those types from snake_case. Our structs still declared snake_case tags, so every multi-word response field silently decoded to its zero value. Single-word fields (id,title,status,effort) kept working, which is why nothing looked obviously broken. Before this change:That's how we get to the 500. Agents deliberately downgrades a tagging fix to
create_ticketwhen the resources aren't Terraform-managed (taggingPrEligibility.ts), butsuggestedActionnever reached the caller — sopreview-fixfell back to its hardcodedopen_pr, andgenerate-action500s for a task with no repo to name.Anything added to the API since June has never worked in the CLI.
What changed
Wire contract
Finding/Task/Action/FindingEvent/FindingTaskEventto camelCase. Request bodies moved too (taskIds,actionType,dismissedReason) — Agents dual-accepts both casings today, and its own comments call our snake_case a compat shim, so this is the safe window to migrate off it.action_id, and the report endpoint'slearning_id/dismissed_action_ids/confirmed_action_ids, are hand-built route literals rather than serialized domain types, so #515 never touched them.has_running_actions/integration_id(no server counterpart at all); addedlifecycleState/remediationState/prUrl(part of the current contract).Paging — findings list is offset-paginated (
page/per_page→{items, pagination}), so--cursorwas a silent no-op pinning every caller to page one. Now--page, and the result carriespage/total_findings/total_pages/next_page.Action type —
preview-fixnow defaults to the task's ownsuggestedAction. Explicit--typestill wins and skips the lookup; a failed lookup or a task with no suggestion falls back toopen_pr; amanualsuggestion errors with what to do instead. The--typeflag defaulted to"open_pr"at the cobra layer, which would have defeated this. MCP tool descriptions updated to tell the agent to omittype.One extra fix in the same path —
create-fixdiscarded the{type, config}envelope'stypeand defaulted toopen_pr, so piping a ticket draft straight through submitted it as a PR. It now honors the drafted type unless--typeis given. This only started to matter once tagging tasks actually drafted tickets.Verified against the live API
A
TestClientGetFindingDecodesCamelCaseFieldscase pins the wire casing against a payload shaped like coast'sFindingDetail, so this can't rot silently again.