Skip to content

fix: anthropic no response#2189

Merged
ehayes2000 merged 1 commit intomainfrom
fix-noresponse
Mar 26, 2026
Merged

fix: anthropic no response#2189
ehayes2000 merged 1 commit intomainfrom
fix-noresponse

Conversation

@ehayes2000
Copy link
Copy Markdown
Contributor

coin flip ass anthropic

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 25, 2026

📝 Walkthrough

Summary by CodeRabbit

  • Refactor
    • Removed the legacy "chained" tool-loop and corresponding public entrypoint, consolidating tool-calling under a single API.
  • New Features
    • Added a new Chat engine that drives streaming chat completions with multi-round tool calling and execution (supports deeper recursion).
  • Tests
    • Adjusted test imports to align with the new chat module layout.

Walkthrough

Removed the Chained-based tool-loop implementation and its ToolLoop::chained accessor; introduced a new Chat<I, T> chat/tool-calling loop and moved MAX_RECURSIONS and TOOL_GENERATOR constants into the chat module (with MAX_RECURSIONS increased).

Changes

Cohort / File(s) Summary
Chained removal
rust/cloud-storage/ai/src/tool/tool_loop/chained.rs, rust/cloud-storage/ai/src/tool/tool_loop/mod.rs, rust/cloud-storage/ai/src/tool/tool_loop/ai_client.rs
Deleted the entire chained.rs implementation (removed struct Chained<I,T> and its public methods) and removed pub mod chained;; removed ToolLoop::chained(&self) from ai_client.rs.
Chat module added
rust/cloud-storage/ai/src/tool/tool_loop/chat/agent.rs, rust/cloud-storage/ai/src/tool/tool_loop/chat/mod.rs, rust/cloud-storage/ai/src/tool/tool_loop/chat/test.rs
Added Chat<I,T> implementation (agent.rs), new chat module with pub use agent::* and public constants MAX_RECURSIONS and TOOL_GENERATOR (mod.rs), and updated test import to super::MAX_RECURSIONS (test.rs).
Constants removed from old location
rust/cloud-storage/ai/src/tool/tool_loop/constant.rs
Removed previously exported MAX_RECURSIONS and TOOL_GENERATOR from constant.rs (imports adjusted accordingly).
🚥 Pre-merge checks | ✅ 1 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Description check ❓ Inconclusive The description 'coin flip ass anthropic' is vague and uses non-descriptive language that does not meaningfully explain any part of the changeset. Provide a clear description of what changes were made and why, relating it to the code modifications in the pull request.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix: anthropic no response' follows the conventional commits format with 'fix:' prefix and is 26 characters, well under the 72-character limit.

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


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

Copy link
Copy Markdown
Contributor

@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: 4

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

Inline comments:
In `@rust/cloud-storage/ai/src/tool/tool_loop/chat/agent.rs`:
- Around line 405-406: Add the #[must_use] attribute to the pure function
correct_message_chain so callers are warned if they ignore its returned
corrected message list; place #[must_use] directly above the function
declaration (the existing #[tracing::instrument(skip_all)] can remain, so add
#[must_use] on its own line immediately above or below the tracing attribute to
ensure the compiler enforces use of the return value from correct_message_chain.
- Line 404: Update the inline comment that currently reads "anthropic sometimes
doesn't return responses for their servier tools. this causes 400's" to correct
the typo "servier" → "server" so it reads "anthropic sometimes doesn't return
responses for their server tools. this causes 400's"; locate and edit the
comment in the agent.rs chat agent code (the comment that begins with "anthropic
sometimes...") to apply this single-word fix.
- Around line 366-372: In the ToolCall::try_from error branch inside the agent
loop, change the tracing field name from err=?e to error=?e in the
tracing::error! invocation so it follows the project's logging convention (i.e.,
use tracing::error!(error=?e, "ToolCall::try_from failed from {:#?}", call)); do
not interpolate the error into the message string.
- Around line 111-117: The loop currently checks for Err with `if let Err(e)`
and then calls `item.unwrap()`, which is non-idiomatic; change the `while let
Some(item) = stream.next().await { ... }` body to use a `match item { Err(e) =>
{ yield Err(e); break 'outer; }, Ok(part_or_ext) => { /* use part_or_ext here */
} }` (or a `let Ok(part_or_ext) = item else { yield Err(e); break 'outer; }`
style) so you never call `.unwrap()` and the `part_or_ext` binding is produced
directly and safely in the `agent.rs` loop handling the stream.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 9ba79930-5441-4089-924b-d102a8e0584c

📥 Commits

Reviewing files that changed from the base of the PR and between c9de926 and 63f2221.

📒 Files selected for processing (7)
  • rust/cloud-storage/ai/src/tool/tool_loop/ai_client.rs
  • rust/cloud-storage/ai/src/tool/tool_loop/chained.rs
  • rust/cloud-storage/ai/src/tool/tool_loop/chat/agent.rs
  • rust/cloud-storage/ai/src/tool/tool_loop/chat/mod.rs
  • rust/cloud-storage/ai/src/tool/tool_loop/chat/test.rs
  • rust/cloud-storage/ai/src/tool/tool_loop/constant.rs
  • rust/cloud-storage/ai/src/tool/tool_loop/mod.rs
💤 Files with no reviewable changes (4)
  • rust/cloud-storage/ai/src/tool/tool_loop/mod.rs
  • rust/cloud-storage/ai/src/tool/tool_loop/ai_client.rs
  • rust/cloud-storage/ai/src/tool/tool_loop/constant.rs
  • rust/cloud-storage/ai/src/tool/tool_loop/chained.rs

Comment on lines +111 to +117
while let Some(item) = stream.next().await {
if let Err(e) = item {
yield Err(e);
break 'outer;
}

let part_or_ext = item.unwrap();
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot Mar 25, 2026

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider using match instead of if let + unwrap().

After checking for Err, the code uses .unwrap() on Line 117. While safe here, this pattern is less idiomatic than using a match or let-else.

Proposed refactor
                         while let Some(item) = stream.next().await {
-                            if let Err(e) = item {
-                                yield Err(e);
-                                break 'outer;
-                            }
-
-                            let part_or_ext = item.unwrap();
+                            let part_or_ext = match item {
+                                Ok(p) => p,
+                                Err(e) => {
+                                    yield Err(e);
+                                    break 'outer;
+                                }
+                            };
                             match part_or_ext {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rust/cloud-storage/ai/src/tool/tool_loop/chat/agent.rs` around lines 111 -
117, The loop currently checks for Err with `if let Err(e)` and then calls
`item.unwrap()`, which is non-idiomatic; change the `while let Some(item) =
stream.next().await { ... }` body to use a `match item { Err(e) => { yield
Err(e); break 'outer; }, Ok(part_or_ext) => { /* use part_or_ext here */ } }`
(or a `let Ok(part_or_ext) = item else { yield Err(e); break 'outer; }` style)
so you never call `.unwrap()` and the `part_or_ext` binding is produced directly
and safely in the `agent.rs` loop handling the stream.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

stfu

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@ehayes2000 Understood, I'll drop it! 🐇

(´• ω •`)ᵒ


🧠 Learnings used
Learnt from: CR
Repo: macro-inc/macro PR: 0
File: rust/cloud-storage/CLAUDE.md:0-0
Timestamp: 2026-03-23T19:07:10.919Z
Learning: Applies to rust/cloud-storage/**/*.rs : Prefer using `.inspect_err()` instead of `if let Err(e)` when doing logging in error handling

Learnt from: CR
Repo: macro-inc/macro PR: 0
File: rust/cloud-storage/CLAUDE.md:0-0
Timestamp: 2026-03-23T19:07:10.919Z
Learning: Applies to rust/cloud-storage/**/*.rs : Prefer `anyhow::bail!("error message")` over `Err(anyhow::anyhow!("error message"))` for early returns with errors

Learnt from: CR
Repo: macro-inc/macro PR: 0
File: rust/cloud-storage/CLAUDE.md:0-0
Timestamp: 2026-03-23T19:07:10.919Z
Learning: Applies to rust/cloud-storage/**/*.rs : Include `err` when adding the `tracing::instrument` attribute to functions that return `Result`. Do not include `err` on functions that return `Option`, `()`, or other non-`Result` types. Never include `level = "info"`

Comment on lines +366 to +372
Err(e) => {
tracing::error!(
err=?e,
"ToolCall::try_from failed from {:#?}",
call
);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Use consistent error field name in tracing.

The coding guidelines specify using error=?e for error logging, but this uses err=?e. This inconsistency can make log querying harder.

Proposed fix
                                     Err(e) => {
                                         tracing::error!(
-                                            err=?e,
+                                            error=?e,
                                             "ToolCall::try_from failed from {:#?}",
                                             call
                                         );
                                     }

As per coding guidelines: "Log errors as tracing::error!(error=?e, "msg") — do not interpolate errors into the message string."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Err(e) => {
tracing::error!(
err=?e,
"ToolCall::try_from failed from {:#?}",
call
);
}
Err(e) => {
tracing::error!(
error=?e,
"ToolCall::try_from failed from {:#?}",
call
);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rust/cloud-storage/ai/src/tool/tool_loop/chat/agent.rs` around lines 366 -
372, In the ToolCall::try_from error branch inside the agent loop, change the
tracing field name from err=?e to error=?e in the tracing::error! invocation so
it follows the project's logging convention (i.e., use tracing::error!(error=?e,
"ToolCall::try_from failed from {:#?}", call)); do not interpolate the error
into the message string.

}
}

// anthropic sometimes doesn't return responses for their servier tools. this causes 400's
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix typo in comment: "servier" → "server".

-// anthropic sometimes doesn't return responses for their servier tools. this causes 400's
+// Anthropic sometimes doesn't return responses for their server tools. This causes 400 errors.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// anthropic sometimes doesn't return responses for their servier tools. this causes 400's
// Anthropic sometimes doesn't return responses for their server tools. This causes 400 errors.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rust/cloud-storage/ai/src/tool/tool_loop/chat/agent.rs` at line 404, Update
the inline comment that currently reads "anthropic sometimes doesn't return
responses for their servier tools. this causes 400's" to correct the typo
"servier" → "server" so it reads "anthropic sometimes doesn't return responses
for their server tools. this causes 400's"; locate and edit the comment in the
agent.rs chat agent code (the comment that begins with "anthropic sometimes...")
to apply this single-word fix.

Comment on lines +405 to +406
#[tracing::instrument(skip_all)]
fn correct_message_chain(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider adding #[must_use] attribute for the pure function.

Since correct_message_chain returns a corrected message list without side effects, adding #[must_use] would prevent accidental ignoring of the return value.

+#[must_use]
 #[tracing::instrument(skip_all)]
 fn correct_message_chain(
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#[tracing::instrument(skip_all)]
fn correct_message_chain(
#[must_use]
#[tracing::instrument(skip_all)]
fn correct_message_chain(
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rust/cloud-storage/ai/src/tool/tool_loop/chat/agent.rs` around lines 405 -
406, Add the #[must_use] attribute to the pure function correct_message_chain so
callers are warned if they ignore its returned corrected message list; place
#[must_use] directly above the function declaration (the existing
#[tracing::instrument(skip_all)] can remain, so add #[must_use] on its own line
immediately above or below the tracing attribute to ensure the compiler enforces
use of the return value from correct_message_chain.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

stfu

Copy link
Copy Markdown
Contributor

@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: 3

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

Inline comments:
In `@rust/cloud-storage/ai/src/tool/tool_loop/chat/agent.rs`:
- Around line 183-190: The tail assistant flush is unconditional and can append
an empty synthetic assistant message; fix by guarding both the server-tool
branch (StreamPart::ToolResponse(ToolResponse::Json { .. })) and the final
unconditional flush (around the code currently at lines ~276-278) so you only
call self.make_assistant_message and push its result when there is actual
content to flush: check that content.is_empty() || tool_calls.is_empty() is
false and also that pending_tool_messages is not empty (i.e., only flush when
content.len() > 0 or !tool_calls.is_empty() or
!pending_tool_messages.is_empty()), then append pending_tool_messages and
messages only in that case to avoid creating an empty assistant turn.
- Around line 19-20: The code currently uses HashMap::into_values() to iterate
tool calls, which loses the model-emitted ordering because HashMap is unordered;
locate where into_values() is used for processing/persisting tool calls (the
places handling ToolCall objects and their call.index) and replace the unordered
iteration with an order-preserving step: collect the values into a Vec, sort
that Vec by each call's call.index (or use an insertion-ordered map like
indexmap::IndexMap keyed by call.index), then iterate the sorted/ordered
collection when executing and persisting tool calls so the final execution order
matches the model emission.
- Around line 360-372: The branch handling ToolCall::try_from failure (inside
the FinishReason::ToolCalls handling) currently logs and drops the malformed
call; instead propagate the failure so the stream and conversation state do not
silently continue—when ToolCall::try_from(call.clone()) returns Err(e) yield or
return an error to the caller (e.g., yield Err(...) or return Err(...)) with
contextual information referencing FinishReason::ToolCalls, ToolCall::try_from,
and the StreamPart/PartOrExt flow so consumers observe the provider error and
the loop terminates rather than swallowing the bad call.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 28dd1615-71dc-48b8-8f0e-11feebcf776b

📥 Commits

Reviewing files that changed from the base of the PR and between 63f2221 and d9ad5f3.

📒 Files selected for processing (7)
  • rust/cloud-storage/ai/src/tool/tool_loop/ai_client.rs
  • rust/cloud-storage/ai/src/tool/tool_loop/chained.rs
  • rust/cloud-storage/ai/src/tool/tool_loop/chat/agent.rs
  • rust/cloud-storage/ai/src/tool/tool_loop/chat/mod.rs
  • rust/cloud-storage/ai/src/tool/tool_loop/chat/test.rs
  • rust/cloud-storage/ai/src/tool/tool_loop/constant.rs
  • rust/cloud-storage/ai/src/tool/tool_loop/mod.rs
💤 Files with no reviewable changes (4)
  • rust/cloud-storage/ai/src/tool/tool_loop/mod.rs
  • rust/cloud-storage/ai/src/tool/tool_loop/ai_client.rs
  • rust/cloud-storage/ai/src/tool/tool_loop/constant.rs
  • rust/cloud-storage/ai/src/tool/tool_loop/chained.rs

Comment on lines +19 to +20
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Preserve the streamed tool-call order.

HashMap::into_values() does not preserve call.index, so multi-tool turns can be executed and persisted in a different order than the model emitted them.

Proposed fix
@@
-use std::collections::{HashMap, HashSet};
+use std::collections::{BTreeMap, HashMap, HashSet};
@@
-            let mut tool_calls: HashMap<u32, PartialToolCall> = HashMap::new();
+            let mut tool_calls: BTreeMap<u32, PartialToolCall> = BTreeMap::new();
@@
-                            tool_calls = HashMap::new();
+                            tool_calls = BTreeMap::new();

Also applies to: 312-356

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

In `@rust/cloud-storage/ai/src/tool/tool_loop/chat/agent.rs` around lines 19 - 20,
The code currently uses HashMap::into_values() to iterate tool calls, which
loses the model-emitted ordering because HashMap is unordered; locate where
into_values() is used for processing/persisting tool calls (the places handling
ToolCall objects and their call.index) and replace the unordered iteration with
an order-preserving step: collect the values into a Vec, sort that Vec by each
call's call.index (or use an insertion-ordered map like indexmap::IndexMap keyed
by call.index), then iterate the sorted/ordered collection when executing and
persisting tool calls so the final execution order matches the model emission.

Comment on lines +183 to +190
StreamPart::ToolResponse(ToolResponse::Json { id, json, .. }) => {
// Server-side tool response - flush current assistant and start new segment
if !content.is_empty() || !tool_calls.is_empty() {
messages.push(
self.make_assistant_message(&mut content, &mut tool_calls),
);
messages.append(&mut pending_tool_messages);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Guard the final assistant flush.

If the server-tool branch already flushed the pending assistant/tool-call segment, the unconditional tail flush appends an empty assistant message. That synthetic turn is then kept in self.messages and replayed on the next request.

Proposed fix
@@
-        // Flush remaining assistant content
-        messages.push(self.make_assistant_message(&mut content, &mut tool_calls));
+        // Flush remaining assistant content
+        if !content.is_empty() || !tool_calls.is_empty() {
+            messages.push(self.make_assistant_message(&mut content, &mut tool_calls));
+        }
         messages.append(&mut pending_tool_messages);

Also applies to: 276-278

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

In `@rust/cloud-storage/ai/src/tool/tool_loop/chat/agent.rs` around lines 183 -
190, The tail assistant flush is unconditional and can append an empty synthetic
assistant message; fix by guarding both the server-tool branch
(StreamPart::ToolResponse(ToolResponse::Json { .. })) and the final
unconditional flush (around the code currently at lines ~276-278) so you only
call self.make_assistant_message and push its result when there is actual
content to flush: check that content.is_empty() || tool_calls.is_empty() is
false and also that pending_tool_messages is not empty (i.e., only flush when
content.len() > 0 or !tool_calls.is_empty() or
!pending_tool_messages.is_empty()), then append pending_tool_messages and
messages only in that case to avoid creating an empty assistant turn.

Comment on lines +360 to +372
if let Some(FinishReason::ToolCalls) = first.finish_reason {
for call in tool_calls.into_values() {
match ToolCall::try_from(call.clone()) {
Ok(call) => {
yield Ok(PartOrExt::Part(StreamPart::ToolCall(call)))
}
Err(e) => {
tracing::error!(
err=?e,
"ToolCall::try_from failed from {:#?}",
call
);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fail fast when a streamed tool call cannot be reconstructed.

This branch only logs and drops the malformed call. The outer loop can then finish as if no tool was requested, which hides a real provider error and leaves the conversation state inconsistent.

Proposed fix
@@
                                     Err(e) => {
                                         tracing::error!(
                                             err=?e,
                                             "ToolCall::try_from failed from {:#?}",
                                             call
                                         );
+                                        yield Err(e.into());
+                                        break;
                                     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if let Some(FinishReason::ToolCalls) = first.finish_reason {
for call in tool_calls.into_values() {
match ToolCall::try_from(call.clone()) {
Ok(call) => {
yield Ok(PartOrExt::Part(StreamPart::ToolCall(call)))
}
Err(e) => {
tracing::error!(
err=?e,
"ToolCall::try_from failed from {:#?}",
call
);
}
if let Some(FinishReason::ToolCalls) = first.finish_reason {
for call in tool_calls.into_values() {
match ToolCall::try_from(call.clone()) {
Ok(call) => {
yield Ok(PartOrExt::Part(StreamPart::ToolCall(call)))
}
Err(e) => {
tracing::error!(
err=?e,
"ToolCall::try_from failed from {:#?}",
call
);
yield Err(e.into());
break;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rust/cloud-storage/ai/src/tool/tool_loop/chat/agent.rs` around lines 360 -
372, The branch handling ToolCall::try_from failure (inside the
FinishReason::ToolCalls handling) currently logs and drops the malformed call;
instead propagate the failure so the stream and conversation state do not
silently continue—when ToolCall::try_from(call.clone()) returns Err(e) yield or
return an error to the caller (e.g., yield Err(...) or return Err(...)) with
contextual information referencing FinishReason::ToolCalls, ToolCall::try_from,
and the StreamPart/PartOrExt flow so consumers observe the provider error and
the loop terminates rather than swallowing the bad call.

@ehayes2000 ehayes2000 merged commit 95743c0 into main Mar 26, 2026
37 of 38 checks passed
@ehayes2000 ehayes2000 deleted the fix-noresponse branch March 26, 2026 18:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant