Hmbown/CodeWhale · error · io::Error

invalid tool image evidence

Error message

invalid tool image evidence

What it means

`publish_image` turns a tool-result image block into persisted rich media evidence. It base64-decodes the block's data and then re-validates the bytes with the shared `decode_and_guard_image` validator; any failure to decode, decode-guard, or even serialize the identity tuple raises this generic `InvalidData` error rather than trusting metadata or headers.

Solutions

  1. Fix the tool/provider so the image block contains valid base64 of a supported image format (check for truncation, whitespace, or wrong encoding).
  2. Re-run the tool call that produced the image to regenerate a clean payload.
  3. Inspect the payload: `base64 -d < data.b64 > img.bin && file img.bin` to see whether the bytes form a real image.
  4. If your pipeline transforms tool output, verify it does not re-encode or corrupt the base64 data before it reaches the engine.

Example fix

// before: trusting/forwarding raw tool payload
data: block.data.clone(),
// after (caller side): validate before publishing
let bytes = STANDARD.decode(&payload).map_err(|_| anyhow::anyhow!("invalid base64"))?;
image::load_from_memory(&bytes)?; // ensure decodable before handing to publish_image
Defensive patterns

Strategy: validation

Validate before calling

use base64::Engine;
fn tool_image_bytes_ok(data: &str) -> Option<Vec<u8>> {
    let bytes = base64::engine::general_purpose::STANDARD.decode(data).ok()?;
    image::load_from_memory(&bytes).ok()?;
    Some(bytes)
}

Type guard

fn is_valid_image_block(block: &ToolResultContentBlock) -> bool {
    matches!(block, ToolResultContentBlock::Image { data, .. })
        && tool_image_bytes_ok(data).is_some()
}

Try / catch

match publish_image(block, session_id, call_id, tool_name) {
    Err(e) if e.kind() == std::io::ErrorKind::InvalidData => {
        log::warn!("tool returned invalid image evidence for {call_id}; skipping media render");
        // degrade gracefully: render the tool result without the image
    }
    other => other?,
}

Prevention

When it happens

Trigger: `project` -> `publish_image` with a `ToolResultContentBlock::Image` whose `data` is not valid base64, whose decoded bytes are not a supported/valid image (per the shared rich-image validator), or with corrupted internal serialization input.

Common situations: A tool (or a provider relaying tool output) emitted a malformed or truncated image payload; the base64 string contains invalid characters or wrong padding; the image is in an unsupported codec or fails dimension/format guards.

Understand the failure class

Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22). Data as JSON: /api/errors/d8634b28f56e8b7f. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/src/core/engine/tool_media.rs:73

    })
    .await
    .unwrap_or_else(|_| {
        let mut rich = RichToolResult::plain(fallback);
        rich.result
            .content
            .push_str("\n[Tool image omitted: image preparation failed.]");
        rich
    })
}

fn publish_image(
    block: &ToolResultContentBlock,
    session_id: &str,
    call_id: &str,
    tool_name: &str,
) -> io::Result<Value> {
    let ToolResultContentBlock::Image { mime_type, data } = block;
    let invalid = || io::Error::new(io::ErrorKind::InvalidData, "invalid tool image evidence");
    let bytes = STANDARD.decode(data).map_err(|_| invalid())?;
    // The caller passed the shared rich-image validator; retain the shared decode
    // guard when deriving dimensions rather than trusting metadata or headers.
    let (_, width, height) =
        crate::image_attach::decode_and_guard_image(&bytes).map_err(|_| invalid())?;
    let identity = serde_json::to_vec(&(session_id, call_id, 0u8)).map_err(|_| invalid())?;
    let handle = format!("art_image_{}", crate::hashing::sha256_hex(&identity));
    let storage_path =
        PathBuf::from(crate::artifacts::ARTIFACTS_DIR_NAME).join(format!("{handle}.image"));
    let digest = crate::hashing::sha256_hex(&bytes);
    let now = unix_millis_now();
    let proposed = EvidenceArtifact {
        handle: handle.clone(),
        digest: digest.clone(),
        size_bytes: bytes.len() as u64,
        content_type: mime_type.clone(),
        tool_name: tool_name.to_owned(),
        call_id: call_id.to_owned(),

View on GitHub (pinned to 73e0f67d83)