Skip to content

fix: exclude expanded tools from prompt diffs - #957

Merged
ctawiah merged 4 commits into
mainfrom
ctawiah/fix-prompt-tool-diff-5b1c
Oct 26, 2026
Merged

ctawiah merged 4 commits into
mainfrom
ctawiah/fix-prompt-tool-diff-5b1c

Conversation

@ctawiah

@ctawiah ctawiah commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Context

LaunchDarkly can return one attached tool in two forms. The response has a top-level tools reference and an expanded model.parameters.tools value.

ldcli treated the expanded copy as a model setting. The plan reported a model change and a pull wrote the expanded tool into the local file.

What changes

  • Normalize response models before planning or applying changes.
  • Reuse the same model canonicalization for fingerprints and rendered diffs.
  • Keep real model parameter changes visible.
  • Add API, fingerprint, plan, output, and apply regression coverage.

The review output now changes as follows:

Before: pull production/support/default  instructions, model changed in LaunchDarkly
After:  pull production/support/default  instructions changed in LaunchDarkly

Design notes

  • Top-level tool and skill references own attachment links and versions.
  • CanonicalModel removes only the API-expanded model.parameters.tools value, and only when the variation has tool references. A legitimate model.parameters.skills value remains.
  • A tools model parameter remains unchanged when the variation has no tool attachment.
  • API decoding normalizes before apply. Fingerprints and diffs call the same helper as a safety measure.

Review focus

  • Can canonicalization hide a real model parameter change?
  • Does a pull keep attachment references and never write expanded tool definitions?

Verification

  • go test ./...
  • go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.9.0 run
  • make build
  • TestPromptDiffExcludesExpandedToolsFromModelParameters failed on main and passes with this change.
  • TestCatalogClientConfigRemovesExpandedToolsFromModelParameters and TestFingerprintVariationExcludesExpandedToolParameters make sure that a model.parameters.skills value stays in the decoded model and changes the fingerprint.

Related changes

This stack builds on #937 to #941, which are merged.

  1. This PR: exclude expanded tools from prompt diffs (ctawiah/fix-prompt-tool-diff-5b1c)
  2. fix: plan a tool or skill row when a variation newly references it #945: Plan a tool or skill row when a variation newly references it (ctawiah/plan-referenced-attachments-1e8a)
  3. feat: keep unused tools and skills, and detach any resource kind #946: Keep unused tools and skills, and detach any resource kind (ctawiah/keep-unused-attachments-1e8a)
  4. feat: add tools and skills from LaunchDarkly #947: Add tools and skills from LaunchDarkly (ctawiah/add-attachments-1e8a)
  5. feat: route sync, add, and detach by scope #948: Route sync, add, and detach by scope (ctawiah/route-scopes-1e8a)
  6. feat: add the sync all, sync tools, and sync skills commands #949: Add the sync all, sync tools, and sync skills commands (ctawiah/scope-commands-1e8a)
  7. feat: create a tool or a skill from the content of an add #950: Create a tool or a skill from the content of an add (ctawiah/create-attachments-1e8a)
  8. feat: add the create flags of sync tools add and sync skills add #951: Add the create flags of sync tools add and sync skills add (ctawiah/create-attachment-flags-1e8a)
  9. refactor: share the selection of a new variation, and check any kind before an add #952: Share the selection of a new variation, and check any kind before an add (ctawiah/share-variation-selection-1e8a)
  10. feat: create a prompt variation from the content of an add #953: Create a prompt variation from the content of an add (ctawiah/create-variations-1e8a)
  11. feat: add the create flags of sync prompts add #954: Add the create flags of sync prompts add (ctawiah/create-variation-flags-1e8a)

Note

Overview
LaunchDarkly often returns the same tools both as top-level tools refs and as expanded model.parameters.tools. ldcli was treating that duplicate as a real model change, so prompt sync showed false model diffs and pulls could persist expanded tool JSON locally.

This PR introduces shared CanonicalModel normalization: when a variation has tool attachments, it strips API-expanded parameters.tools while keeping other parameters (including parameters.skills), still dropping empty parameters/custom objects. That runs when decoding catalog config responses, when fingerprinting variations, and when rendering variation JSON for diffs—so planning, pull/push, and manifest fingerprints agree.

Variation validation now rejects non-object model.parameters and model.custom before writes, matching LaunchDarkly’s object-only semantics.

Regression tests cover catalog decode, fingerprints, plan/diff output, and end-to-end prompt sync (including simulated expandTools API behavior).

Reviewed by Cursor Bugbot for commit 4e0e87b. Bugbot is set up for automated code reviews on this repo. Configure here.

for _, key := range []string{"parameters", "custom"} {
if value, ok := model[key].(map[string]any); ok && len(value) == 0 {
delete(model, key)
if parameters, ok := model["parameters"].(map[string]any); ok {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

We should validate user's input parameters as an object here I think.
In case someone does:

key: default
name: Default
model:
  modelName: gpt-5
  parameters: temperature=0.2
---

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Agreed. Your example synced without an error, and ldcli sent "parameters": "temperature=0.2" as a string. LaunchDarkly does not keep a non-object value there, so the setting was lost. Variation.Validate now rejects a non-object model.parameters or model.custom with model.parameters must be an object (or model.custom ...) before any write. TestPromptRejectsModelValuesThatAreNotObjects covers your example, an empty parameters:, and a list in custom, and it failed before the fix. Added in f001a10.

requireOnlyReads(t, api.requests)
}

func TestPromptDiffExcludesExpandedToolsFromModelParameters(t *testing.T) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This seems to cover only pull. think it would be worth it for a push test case?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Good call, added TestPromptPushExcludesExpandedToolsFromModelParameters. The fake API can now add the expanded tools to model.parameters.tools on every read, as LaunchDarkly does. The test changes only the local instructions and checks that the plan says instructions changed with no model change. It then pushes, checks that LaunchDarkly gets the local model without the expanded tools, and checks that the next sync reports No changes.. Without this PR's fix, the same test fails with a conflict, because the expanded tools look like a model change in LaunchDarkly. Added in 4e0e87b.

@nkester-ld nkester-ld left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Might be worth the extra validation and test cases

LaunchDarkly keeps model.parameters and model.custom only as objects. Sync now rejects any other value before it writes.
The test of expanded tools covered only a pull. The fake API can now add the tool content to model.parameters.tools on each read, as LaunchDarkly does.
@ctawiah
ctawiah merged commit 438c0d1 into main Oct 26, 2026
10 checks passed
@ctawiah
ctawiah deleted the ctawiah/fix-prompt-tool-diff-5b1c branch October 26, 2026 19:54
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