gitbutlerapp/gitbutler · error

validated AI responses only produce content picks

Error message

validated AI responses only produce content picks

What it means

In but-api's merge-conflict AI resolution, `validate_ai_response` narrows the model's output to hunk picks, and the apply loop then asserts every pick is `HunkPick::Content`. The `unreachable!` fires if a validated pick is any other variant (e.g. a deletion), meaning the validator's contract ('only content picks survive validation') and the actual pick kinds disagree. It is a contract break between `validate_ai_response` and the consumer, not an AI-output error per se.

Source

Thrown at crates/but-api/src/resolve/mod.rs:452

                        .map(|&(_, hunk_index)| file.hunks[hunk_index].clone())
                        .collect(),
                }
            })
            .collect(),
    };

    let picks = retry_once(|| {
        let response = resolve(&narrowed)?;
        let (picks, _files) = validate_ai_response(&narrowed, &response)?;
        Ok(picks)
    })?;

    // The narrowed files and the validated picks are aligned index-for-index,
    // and within a file the picks' 0..n keys match the sorted targets.
    for (targets, file_picks) in targets_per_file.values().zip(picks) {
        for (&(spec_index, _), (_, pick)) in targets.iter().zip(file_picks) {
            let apply::HunkPick::Content(content) = pick else {
                unreachable!("validated AI responses only produce content picks");
            };
            specs[spec_index].resolution = HunkResolution::Content(content);
        }
    }
    Ok(true)
}

/// How one conflicted file was resolved, for display to the user.
#[derive(Debug, Clone, Serialize)]
#[cfg_attr(feature = "export-schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "camelCase")]
pub struct ResolvedFile {
    /// The repo-relative path of the file.
    pub path: String,
    /// The content that replaced each conflict block, in file order.
    pub hunks: Vec<String>,
    /// The model's explanation of its decision for this file.
    pub reasoning: String,

View on GitHub (pinned to caf1f223d3)

Solutions

  1. Keep `validate_ai_response` rejecting every non-`Content` pick so the consumer's assertion holds.
  2. If deletions must be supported, replace the `unreachable!` with an explicit arm that maps the pick into `HunkResolution` or returns an error.
  3. Add a unit test that feeds a deletion pick through validation to pin the contract.

Example fix

// before
let apply::HunkPick::Content(content) = pick else {
    unreachable!("validated AI responses only produce content picks");
};

// after
match pick {
    apply::HunkPick::Content(content) => {
        specs[spec_index].resolution = HunkResolution::Content(content);
    }
    other => {
        return Err(anyhow::"validated pick was not a content pick: {other:?}"));
    }
}
Defensive patterns

Strategy: type-guard

Validate before calling

// pin the validator contract before applying picks
let all_content = picks.iter().flatten().all(|p| matches!(p, apply::HunkPick::Content(_)));
if !all_content {
    return Ok(false); // or error: validation let a non-content pick through
}

Type guard

fn content_pick(pick: &apply::HunkPick) -> Option<&apply::hunk::Content> {
    match pick {
        apply::HunkPick::Content(c) => Some(c),
        _ => None,
    }
}

Prevention

When it happens

Trigger: `validate_ai_response` is extended to pass through deletion picks while the consumer still matches only `Content`; a schema/enum change adds a new `HunkPick` variant that validation does not reject; zip misalignment pairs picks with unexpected file slots.

Common situations: Evolving the resolution API to support delete-style resolutions; regenerating types for the SDK boundary; mismatches after refactoring the narrowed-file/pick index alignment.

Related errors


AI-assisted analysis of gitbutlerapp/gitbutler@caf1f223d3 (2026-08-20). Data as JSON: /api/errors/f8b67c13ba8a2df0. Report an issue: GitHub.