Skip to content

fix: image content handling in loop#355

Merged
qhkm merged 5 commits intoqhkm:mainfrom
rafaellin:fix/resolve-images-in-tool-loops
Mar 16, 2026
Merged

fix: image content handling in loop#355
qhkm merged 5 commits intoqhkm:mainfrom
rafaellin:fix/resolve-images-in-tool-loops

Conversation

@rafaellin
Copy link
Copy Markdown
Contributor

@rafaellin rafaellin commented Mar 13, 2026

Summary

Related Issue

Closes #

Scope

  • I branched from upstream/main
  • This PR contains only commits related to this change
  • cargo test passes
  • cargo clippy -- -D warnings passes
  • cargo fmt --check passes

Test Plan

Summary by CodeRabbit

  • Bug Fixes

    • Images are reliably converted to inline/base64 before processing, preventing loss across interaction modes.
    • Empty user messages that become meaningful after image resolution are preserved rather than dropped.
    • Message preparation is now consistent across initial, streaming, tool-loop, and final/synthesis paths.
  • Refactor

    • Centralized message preprocessing to ensure uniform behavior and simplify maintenance.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 13, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Added a private helper that centralizes message construction, resolves FilePath images to Base64 asynchronously, filters empty user messages after resolution, and replaces prior inline constructions across initial, tool-loop, and final LLM interaction paths; tests updated to async where needed.

Changes

Cohort / File(s) Summary
Message construction & image resolution
src/agent/loop.rs
Added private fn build_resolved_messages(&self, session: &crate::session::Session, memory_override: Option<&str>) -> Vec<Message> (async image resolution internally). Replaced earlier inline build_messages_with_memory_override usages so all LLM paths use the new helper and empty-user-message filtering occurs after resolution.
Tests (async updates)
tests/... (converted tests)
Updated tests that call image resolution to be async tokio tests and awaited resolve_images_to_base64 calls.

Sequence Diagram

sequenceDiagram
    participant AgentLoop as Agent Loop
    participant Builder as build_resolved_messages
    participant ImageResolver as resolve_images_to_base64
    participant Provider as LLM Provider

    AgentLoop->>Builder: request resolved messages (session, memory_override?)
    activate Builder
    Builder->>Builder: build messages with memory override
    Builder->>ImageResolver: async resolve FilePath images -> base64
    activate ImageResolver
    ImageResolver-->>Builder: messages with inline/base64 images
    deactivate ImageResolver
    Builder->>Builder: filter empty user messages (post-resolution)
    Builder-->>AgentLoop: resolved & filtered messages
    deactivate Builder
    AgentLoop->>Provider: send resolved messages
    activate Provider
    Provider-->>AgentLoop: LLM response
    deactivate Provider
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 I hop through loops and stitch each thread,

I turn file paths into colors unread.
Empty lines checked after pictures are done,
base64 tucked snug — now the flow runs.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: image content handling in loop' directly and clearly summarizes the main change—consolidating image handling logic and fixing filter ordering to ensure images are resolved before empty-message filtering.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@rafaellin rafaellin changed the title fix/image content handling in loop fix: image content handling in loop Mar 13, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/agent/loop.rs`:
- Around line 1482-1497: The current message filtering (using Role::User and
has_images()) runs before resolve_images_to_base64, so unreadable images may be
dropped and leave empty user messages; after calling
resolve_images_to_base64(&mut msgs, dir) re-run the same filter (e.g., retain or
a second filter) on msgs to remove any Role::User entries with empty content and
no images so the final messages Vec (used later) contains no blank user turns;
locate this around build_messages_with_memory_override -> msgs and
resolve_images_to_base64 in loop.rs and apply the additional filtering step.
- Around line 1493-1495: The call to resolve_images_to_base64(&mut msgs, dir)
blocks the Tokio runtime because it does sync fs reads; change this to run on a
blocking thread: refactor resolve_images_to_base64 (or add a new function e.g.,
resolve_images_to_base64_blocking) to accept owned messages (or return an owned
Vec) and perform the fs reads synchronously, then from process_message and
process_message_streaming replace the direct call with
tokio::task::spawn_blocking(move || resolve_images_to_base64_blocking(msgs,
dir)).await?? to get back the processed msgs and assign them into msgs; ensure
you stop taking an &mut msgs reference across the await by moving/returning
ownership and keep using session_manager.sessions_dir() to supply dir.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1e46105d-63db-434e-9acf-232deef23c61

📥 Commits

Reviewing files that changed from the base of the PR and between 3976378 and 2132b22.

📒 Files selected for processing (1)
  • src/agent/loop.rs

Comment thread src/agent/loop.rs Outdated
Comment thread src/agent/loop.rs Outdated
@qhkm
Copy link
Copy Markdown
Owner

qhkm commented Mar 14, 2026

Review

The core fix is correct — images need to survive filtering and be resolved to base64 for provider compatibility. A couple of things to address before merge:

1. Filter ordering bug (agrees with CodeRabbit)

The filter !(m.role == Role::User && m.content.is_empty() && !m.has_images()) runs before resolve_images_to_base64. If an image file is unreadable (line 350: silently dropped), the message ends up with empty content AND no images — a blank user turn sent to the provider.

Fix: move the filter to after resolution at all 4 call sites:

resolve_images_to_base64(&mut msgs, dir);
msgs.retain(|m| !(m.role == Role::User && m.content.is_empty() && !m.has_images()));

2. Code duplication

The same 12-line block (build messages → filter → resolve → collect) is copy-pasted 4 times. Consider extracting into a helper method on AgentLoop to prevent the copies from drifting out of sync:

fn build_resolved_messages(&self, session: &Session, memory_override: Option<&str>) -> Vec<Message> { ... }

3. Missing linked issue

PR template has Closes # with no issue number — please link the relevant issue.

@rafaellin
Copy link
Copy Markdown
Contributor Author

Closed #368

When the agent executes tools in a conversation with images, image file
paths were not being resolved to base64 in subsequent LLM API calls during
the tool execution loop. This caused the LLM to receive file path references
instead of actual image data.

This fix:
- Adds build_resolved_messages() helper to centralize message preparation
- Resolves image paths to base64 before each tool loop LLM call
- Filters empty messages AFTER resolution (if image load fails)
- Applies fix to all 4 tool loop locations (streaming + non-streaming)

Fixes: Images now persist through entire tool execution loops
@rafaellin rafaellin force-pushed the fix/resolve-images-in-tool-loops branch from 3064077 to 10aab9c Compare March 16, 2026 07:13
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
src/agent/loop.rs (1)

2404-2429: Consider reusing this helper for the initial provider calls too.

process_message and process_message_streaming still duplicate message build/resolve logic before the first provider.chat(...). Routing those through build_resolved_messages would keep one canonical behavior and prevent future drift.

♻️ Suggested simplification
- let mut messages = self.context_builder.build_messages_with_memory_override(
-     &session.messages,
-     "",
-     memory_override.as_deref(),
- );
- if let Some(dir) = self.session_manager.sessions_dir() {
-     resolve_images_to_base64(&mut messages, dir);
- }
+ let messages = self.build_resolved_messages(&session, memory_override.as_deref());
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/agent/loop.rs` around lines 2404 - 2429, The duplicate message-building
and image-resolution logic in process_message and process_message_streaming
should be replaced by a call to build_resolved_messages to centralize behavior:
in both functions, remove the local build_messages_with_memory_override +
resolve_images_to_base64 + retain filter logic and instead call
self.build_resolved_messages(&session, memory_override) to obtain msgs before
the first provider.chat(...) call; ensure you pass the same session value and
memory_override used previously and that build_resolved_messages still uses
self.session_manager.sessions_dir() for image resolution so semantics remain
identical.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/agent/loop.rs`:
- Around line 2404-2429: The duplicate message-building and image-resolution
logic in process_message and process_message_streaming should be replaced by a
call to build_resolved_messages to centralize behavior: in both functions,
remove the local build_messages_with_memory_override + resolve_images_to_base64
+ retain filter logic and instead call self.build_resolved_messages(&session,
memory_override) to obtain msgs before the first provider.chat(...) call; ensure
you pass the same session value and memory_override used previously and that
build_resolved_messages still uses self.session_manager.sessions_dir() for image
resolution so semantics remain identical.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e49edaa0-6bca-4b5b-8fbe-ea5b065e1138

📥 Commits

Reviewing files that changed from the base of the PR and between 2132b22 and 3064077.

📒 Files selected for processing (1)
  • src/agent/loop.rs

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
src/agent/loop.rs (1)

4041-4122: Add one helper-level regression test for the exact blank-turn scenario.

Current tests validate resolve_images_to_base64, but they don’t directly assert build_resolved_messages post-resolution filtering behavior (the logic used by all call paths). A focused test here would lock in the PR’s core fix and prevent reintroducing filter-order regressions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/agent/loop.rs` around lines 4041 - 4122, Add a focused regression test
that ensures build_resolved_messages drops an entire blank turn created when
resolve_images_to_base64 removes unreadable image parts: create a Message
containing only a FilePath image pointing at a missing file, call
resolve_images_to_base64(&mut messages, tmp.path()).await, then call
build_resolved_messages or the public API that uses it and assert the resulting
resolved message list does not include an empty/blank turn (e.g., length is 0 or
the message was removed); reference the resolve_images_to_base64 and
build_resolved_messages helper names so the test validates the exact blank-turn
scenario fixed by the PR.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/agent/loop.rs`:
- Around line 4041-4122: Add a focused regression test that ensures
build_resolved_messages drops an entire blank turn created when
resolve_images_to_base64 removes unreadable image parts: create a Message
containing only a FilePath image pointing at a missing file, call
resolve_images_to_base64(&mut messages, tmp.path()).await, then call
build_resolved_messages or the public API that uses it and assert the resulting
resolved message list does not include an empty/blank turn (e.g., length is 0 or
the message was removed); reference the resolve_images_to_base64 and
build_resolved_messages helper names so the test validates the exact blank-turn
scenario fixed by the PR.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7d7df680-8953-461f-9918-cc7d58ec0a07

📥 Commits

Reviewing files that changed from the base of the PR and between b6866a5 and 08c372d.

📒 Files selected for processing (1)
  • src/agent/loop.rs

@qhkm qhkm merged commit 3215bba into qhkm:main Mar 16, 2026
9 checks passed
taqtiqa-mark pushed a commit to taqtiqa-mark/zeptoclaw that referenced this pull request Mar 25, 2026
## Summary

<!-- 1-3 bullet points describing what this PR does -->

## Related Issue

<!-- Link the issue this PR addresses. Use "Closes #N" to auto-close on
merge. -->
<!-- If no issue exists, explain why (e.g. typo fix, trivial refactor).
-->

Closes #

## Scope

- [ ] I branched from `upstream/main`
- [ ] This PR contains only commits related to this change
- [ ] `cargo test` passes
- [ ] `cargo clippy -- -D warnings` passes
- [ ] `cargo fmt --check` passes

## Test Plan

<!-- How did you verify this works? -->


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Images are reliably converted to inline/base64 before processing,
preventing loss across interaction modes.
* Empty user messages that become meaningful after image resolution are
preserved rather than dropped.
* Message preparation is now consistent across initial, streaming,
tool-loop, and final/synthesis paths.

* **Refactor**
* Centralized message preprocessing to ensure uniform behavior and
simplify maintenance.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
@coderabbitai coderabbitai bot mentioned this pull request Mar 25, 2026
5 tasks
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