Skip to content

Refactor/bare loop - #70

Merged
bobrykov merged 5 commits into
masterfrom
refactor/bare-loop
Aug 6, 2026
Merged

Refactor/bare loop#70
bobrykov merged 5 commits into
masterfrom
refactor/bare-loop

Conversation

@bobrykov

@bobrykov bobrykov commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

No description provided.

@dch-labs dch-labs deleted a comment from coderabbitai Bot Aug 5, 2026
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 6b68bdc3-c560-4217-a7c0-9f19bf09c454

📥 Commits

Reviewing files that changed from the base of the PR and between 7808476 and 922a2f4.

📒 Files selected for processing (3)
  • src/engine/bare/llm_turn.rs
  • src/engine/bare/tests.rs
  • src/stream/handler.rs
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/stream/handler.rs
  • src/engine/bare/tests.rs
  • src/engine/bare/llm_turn.rs

📝 Walkthrough

Walkthrough

The refactor moves BareLoop execution onto core machine APIs. It adds turn modes, structured lifecycle emission, ordered tool dispatch, recovery limits, configurable memory retrieval, model switching, expanded builders, feature guards, stream timeout updates, and default-feature Clippy validation.

Changes

BareLoop execution refactor

Layer / File(s) Summary
Execution contracts and configuration
src/engine/core/*, src/error.rs, src/engine/bare/config.rs, src/lib.rs, src/engine.rs, src/contributor.rs
Adds memory_top_k, TurnMode, 0-indexed tool turns, outcome translation, recovery exhaustion errors, configuration builders, and contributor exports.
LLM turns and lifecycle emission
src/engine/bare.rs, src/engine/bare/llm_turn.rs, src/engine/bare/emission.rs, src/engine/bare/model_switch.rs, src/engine/bare/compact.rs
Separates LLM-turn execution, streaming, accounting, memory retrieval, model switching, terminal handling, observer events, and hook events.
Tool dispatch and recovery
src/engine/bare/dispatch.rs
Adds explicit recovery decisions, direct ToolDispatchResult handling, ordered mixed-tool results, cancellation behavior, parallel-wave semantics, and recovery-ceiling handling.
Validation and release records
src/engine/bare/tests.rs, .github/workflows/ci.yml, Makefile, src/reflection/llm.rs, src/tool.rs, tests/provider_e2e.rs, src/stream/handler.rs, src/managers.rs, CHANGELOG.md
Adds execution coverage, default-feature Clippy checks, feature gates, provider test guards, shorter stream timeouts, and changelog entries.

Possibly related PRs

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the primary change as a refactor of the BareLoop implementation.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 50.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/bare-loop

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.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/engine/bare/dispatch.rs (1)

414-424: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Abort the wave on the first hard error.

futures::future::join_all(tasks) waits for every in-wave future and only collects results after all complete. If one call reaches LoopError::ToolRecoveryExhausted while another call is still running long work, the parallel batch delays the hard error and violates the documented first-hard-error-abort semantics. Use a short-circuit collector such as futures::future::try_join_all so the first Err immediately stops pending sibling futures.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/engine/bare/dispatch.rs` around lines 414 - 424, Update the wave
collection in the dispatch loop around `join_all(tasks)` to use a
short-circuiting collector such as `try_join_all`, so the first hard error
aborts pending sibling futures immediately. Preserve handling for cancellation
and successful results, and continue storing completed results in the correct
`idx` slots before returning the error.

Source: MCP tools

🧹 Nitpick comments (5)
src/engine/bare.rs (3)

1075-1081: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Replace the match with ?.

The Err arm returns the error unchanged, so the ? operator expresses the same control flow.

♻️ Proposed refactor
-        let dispatched_parts: Vec<MessagePart> = match self
-            .dispatch_and_record(&dispatch_calls, turn, &accounting)
-            .await
-        {
-            Ok(parts) => parts,
-            Err(e) => return Err(e),
-        };
+        let dispatched_parts: Vec<MessagePart> = self
+            .dispatch_and_record(&dispatch_calls, turn, &accounting)
+            .await?;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/engine/bare.rs` around lines 1075 - 1081, Replace the match handling
around dispatch_and_record in the current method with the ? operator, preserving
the existing successful Vec<MessagePart> assignment and unchanged error
propagation.

941-946: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Read memory_top_k through current_run().

The rest of the refactor reads the in-flight run through self.session.current_run() / current_run_mut(). This site still indexes self.session.runs.last() directly. The result is the same today, but the direct field access bypasses the accessor and will drift if the "current run" definition changes.

♻️ Proposed refactor
         let memory_top_k = self
             .session
-            .runs
-            .last()
+            .current_run()
             .map_or(RunConfig::default().memory_top_k, |r| r.config.memory_top_k);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/engine/bare.rs` around lines 941 - 946, Update collect_memories to obtain
memory_top_k through self.session.current_run() instead of directly accessing
self.session.runs.last(). Preserve the existing default fallback when no current
run exists and continue reading the current run’s config.memory_top_k.

1169-1185: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider delegating the error arms to MachineOutcome::to_loop_error.

stop_reason at Line 1252 already converts a terminal outcome into a LoopError through MachineOutcome::to_loop_error(max_turns). This arm re-implements the same three mappings inline. Only the Completed arm is special here, because it stores final_text and breaks. Delegating the remaining arms keeps one mapping definition.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/engine/bare.rs` around lines 1169 - 1185, The MachineStep::Done handling
duplicates MachineOutcome-to-LoopError mappings. Keep the Completed arm’s output
storage and break behavior, but replace the MaxTurnsExceeded, Cancelled, and
Failed arms with MachineOutcome::to_loop_error using run_config.max_turns,
matching the existing stop_reason conversion.
src/engine/bare/emission.rs (1)

249-258: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

The stream events derive turn from a different source than the rest of the refactor.

record_turn_success and record_turn_failure compute turn as self.session.current_run().map_or(0, Run::turn_count). Every other notification in this layer now carries the machine-supplied turn from MachineStep::CallLLM. The two agree today, because handle_call_llm pushes the Turn only after the provider call returns, so turn_count() still equals the 0-indexed current turn. The equality is incidental, not enforced.

Passing the machine turn down into these helpers would make on_stream_success / on_stream_failure correlate with on_turn_start by construction.

Also applies to: 293-299

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/engine/bare/emission.rs` around lines 249 - 258, Update
record_turn_success and record_turn_failure to accept the machine-supplied turn
as an argument and use it when constructing StreamContext, replacing the current
session.current_run().map_or(0, Run::turn_count) lookup. Propagate the turn from
MachineStep::CallLLM through both helper call sites so stream success and
failure notifications correlate with on_turn_start.
src/engine/bare/tests.rs (1)

1273-1281: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

State the "no previous event" case explicitly.

idx.wrapping_sub(1) relies on usize wraparound to usize::MAX and then on Vec::get returning None for that index. The intent is "there is no event before index 0". checked_sub expresses it directly.

♻️ Proposed refactor
-        let before = events.get(idx.wrapping_sub(1));
+        let before = idx.checked_sub(1).and_then(|i| events.get(i));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/engine/bare/tests.rs` around lines 1273 - 1281, Update the event-boundary
assertion around the on_compaction search to use checked_sub when computing the
preceding index, explicitly representing the absence of a previous event at
index 0 instead of relying on wrapping subtraction. Preserve the existing
before/after boundary checks and assertion behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/engine/bare.rs`:
- Around line 990-1009: Update turn_input to take &self instead of &mut self,
and use self.machine.full_history().last() for non-initial turns so pending
user, assistant, and tool messages are included. Preserve the existing
first-turn current_run input behavior and text-part aggregation.
- Around line 828-852: Ensure every error path after notify_turn_start in the
turn-processing flow emits notify_turn_end before returning. Update the
non-cancelled do_turn error arm and the apply_loop_detection error branch to
send a failed TurnEnd with the relevant error details, duration, and token
counts, matching the existing LoopError::Cancelled handling while preserving
their original error returns.

In `@src/engine/bare/dispatch.rs`:
- Around line 771-774: Update the tokio::select! in recovery_wait_or_return to
use biased selection, placing self.cancelled.notified() before the recovery
sleep branch. Preserve RecoveryDecision::Cancelled when cancellation is ready,
ensuring cancellation wins over a simultaneously ready Retry.
- Around line 553-557: Preserve the initial tool name across recovery retries in
the dispatch flow containing the recovery loop: capture the original `ToolCall`
name before retries can mutate `tc.tool`, then use that saved value for
`LoopError::ToolRecoveryExhausted` instead of the current corrected name.

In `@src/engine/bare/llm_turn.rs`:
- Around line 98-120: Update do_create_message to race
client.create_message_with_options against a configurable timeout in addition to
cancellation, using the existing timeout configuration used by the streaming
path where available. Map timeout expiration to LoopError::Api (or the
established dedicated timeout variant) before passing it through
record_turn_failure, while preserving the existing success and cancellation
behavior.

In `@src/engine/bare/model_switch.rs`:
- Around line 93-114: Handle the boolean result from ApiClient::set_model in
apply instead of unconditionally reporting success: when it returns false,
either emit a tracing warning while preserving the existing best-effort fallback
and observer behavior, or return LoopError::Config and skip those updates,
consistent with the intended design and switch_model_unsupported_client. Also
revise apply’s “atomically” documentation to reflect that no rollback occurs
when model switching fails.

In `@src/engine/core/lifecycle.rs`:
- Around line 100-106: Update BareLoop::collect_memories to return immediately
when memory_top_k is zero, before accessing or invoking the LoopMemory backend;
preserve normal retrieval for positive values. Add a test using a tracking/mock
backend to verify retrieve is not called when memory_top_k is zero.

---

Outside diff comments:
In `@src/engine/bare/dispatch.rs`:
- Around line 414-424: Update the wave collection in the dispatch loop around
`join_all(tasks)` to use a short-circuiting collector such as `try_join_all`, so
the first hard error aborts pending sibling futures immediately. Preserve
handling for cancellation and successful results, and continue storing completed
results in the correct `idx` slots before returning the error.

---

Nitpick comments:
In `@src/engine/bare.rs`:
- Around line 1075-1081: Replace the match handling around dispatch_and_record
in the current method with the ? operator, preserving the existing successful
Vec<MessagePart> assignment and unchanged error propagation.
- Around line 941-946: Update collect_memories to obtain memory_top_k through
self.session.current_run() instead of directly accessing
self.session.runs.last(). Preserve the existing default fallback when no current
run exists and continue reading the current run’s config.memory_top_k.
- Around line 1169-1185: The MachineStep::Done handling duplicates
MachineOutcome-to-LoopError mappings. Keep the Completed arm’s output storage
and break behavior, but replace the MaxTurnsExceeded, Cancelled, and Failed arms
with MachineOutcome::to_loop_error using run_config.max_turns, matching the
existing stop_reason conversion.

In `@src/engine/bare/emission.rs`:
- Around line 249-258: Update record_turn_success and record_turn_failure to
accept the machine-supplied turn as an argument and use it when constructing
StreamContext, replacing the current session.current_run().map_or(0,
Run::turn_count) lookup. Propagate the turn from MachineStep::CallLLM through
both helper call sites so stream success and failure notifications correlate
with on_turn_start.

In `@src/engine/bare/tests.rs`:
- Around line 1273-1281: Update the event-boundary assertion around the
on_compaction search to use checked_sub when computing the preceding index,
explicitly representing the absence of a previous event at index 0 instead of
relying on wrapping subtraction. Preserve the existing before/after boundary
checks and assertion behavior.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 23a20ab2-fb17-4287-90ab-df359c7a89ac

📥 Commits

Reviewing files that changed from the base of the PR and between e3a334a and 603eeba.

📒 Files selected for processing (27)
  • .github/workflows/ci.yml
  • CHANGELOG.md
  • Makefile
  • src/contributor.rs
  • src/engine.rs
  • src/engine/bare.rs
  • src/engine/bare/compact.rs
  • src/engine/bare/config.rs
  • src/engine/bare/dispatch.rs
  • src/engine/bare/emission.rs
  • src/engine/bare/llm_turn.rs
  • src/engine/bare/message.rs
  • src/engine/bare/model_switch.rs
  • src/engine/bare/stream.rs
  • src/engine/bare/tests.rs
  • src/engine/core.rs
  • src/engine/core/lifecycle.rs
  • src/engine/core/machine.rs
  • src/engine/core/outcome.rs
  • src/error.rs
  • src/lib.rs
  • src/memory/builtin.rs
  • src/message.rs
  • src/presets.rs
  • src/reflection/llm.rs
  • src/tool.rs
  • tests/provider_e2e.rs
💤 Files with no reviewable changes (4)
  • src/engine.rs
  • src/engine/bare/message.rs
  • src/engine/bare/stream.rs
  • src/memory/builtin.rs

Comment thread src/engine/bare.rs Outdated
Comment thread src/engine/bare.rs Outdated
Comment thread src/engine/bare/dispatch.rs
Comment thread src/engine/bare/dispatch.rs
Comment thread src/engine/bare/llm_turn.rs
Comment thread src/engine/bare/model_switch.rs
Comment thread src/engine/core/lifecycle.rs

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/engine/bare/dispatch.rs (2)

768-776: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Make recovery analysis cancellation-aware.

recover_tool_error(...).await runs Reflector::analyze() and RecoveryStrategy::decide() before the cancellation-aware select!. If either await blocks a cancellable path, return RecoveryDecision::Cancelled only after that work finishes. Wrap this await in a biased select! with self.cancelled.notified() first, then keep the existing backoff select! as-is.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/engine/bare/dispatch.rs` around lines 768 - 776, Make the await of
recover_tool_error cancellation-aware by wrapping it in a biased tokio::select!,
placing self.cancelled.notified() first and returning
RecoveryDecision::Cancelled when cancellation fires. Preserve the resulting
recovery_action/correction handling and leave the existing backoff select!
unchanged.

414-424: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Abort sibling futures when a wave gets a hard error.

join_all waits for a wave’s futures to complete. If one call returns LoopError::LoopDetected or LoopError::ToolRecoveryExhausted, a sibling call can continue until join_all finishes, delaying the batch result and allowing sibling tool side effects after the hard stop.

Use a fail-fast or cancellable parallel-collection pattern for each wave, then preserve result order for successful waves. Update parallel_hard_error_discards_sibling_results to use max_concurrency: 2, a non-cancellation hard error, and a delayed sibling.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/engine/bare/dispatch.rs` around lines 414 - 424, Replace the
`join_all(tasks).await` collection in the wave-dispatch flow with a fail-fast,
cancellable pattern that aborts sibling futures when a non-cancellation hard
error such as `LoopDetected` or `ToolRecoveryExhausted` occurs. Preserve ordered
result placement for successful outcomes via the existing `wave` indices and
keep cancellation handling intact. Update
`parallel_hard_error_discards_sibling_results` to configure `max_concurrency:
2`, use a non-cancellation hard error, and include a delayed sibling verifying
it is cancelled.
🧹 Nitpick comments (1)
src/engine/bare/tests.rs (1)

1005-1032: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a positive control so the test cannot pass for the wrong reason.

TrackingMemory::len() returns 0. If the engine skips retrieval whenever memory is empty, retrieve_calls == 0 holds even when memory_top_k > 0. The assertion then proves nothing about memory_top_k.

Return a non-zero len(), or add a paired case with memory_top_k: 3 that asserts retrieve_calls == 1.

♻️ Proposed change
         fn len(&self) -> usize {
-            0
+            1
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/engine/bare/tests.rs` around lines 1005 - 1032, Strengthen the
memory_top_k retrieval test around TrackingMemory by making len() report
available memory or adding a paired positive-control run with memory_top_k set
to 3. Assert that the positive case invokes retrieve exactly once, while
preserving the existing assertion that memory_top_k == 0 skips retrieval.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/engine/bare/llm_turn.rs`:
- Around line 112-118: Update the timeout handling in do_create_message and
turn_timeout so an unconfigured handler never passes Duration::MAX to
tokio::time::sleep. When no stream timeout is configured, use a finite per-turn
timeout or bypass this timeout path for default passthrough handlers, while
preserving cancellation behavior.

In `@src/stream/handler.rs`:
- Around line 114-116: Adjust the default timeout values in the stream handler
configuration so per_event_timeout is strictly shorter than
total_stream_timeout, preserving total_stream_timeout at five minutes and
enabling the per-event timeout path used by event_deadline and next_event.

---

Outside diff comments:
In `@src/engine/bare/dispatch.rs`:
- Around line 768-776: Make the await of recover_tool_error cancellation-aware
by wrapping it in a biased tokio::select!, placing self.cancelled.notified()
first and returning RecoveryDecision::Cancelled when cancellation fires.
Preserve the resulting recovery_action/correction handling and leave the
existing backoff select! unchanged.
- Around line 414-424: Replace the `join_all(tasks).await` collection in the
wave-dispatch flow with a fail-fast, cancellable pattern that aborts sibling
futures when a non-cancellation hard error such as `LoopDetected` or
`ToolRecoveryExhausted` occurs. Preserve ordered result placement for successful
outcomes via the existing `wave` indices and keep cancellation handling intact.
Update `parallel_hard_error_discards_sibling_results` to configure
`max_concurrency: 2`, use a non-cancellation hard error, and include a delayed
sibling verifying it is cancelled.

---

Nitpick comments:
In `@src/engine/bare/tests.rs`:
- Around line 1005-1032: Strengthen the memory_top_k retrieval test around
TrackingMemory by making len() report available memory or adding a paired
positive-control run with memory_top_k set to 3. Assert that the positive case
invokes retrieve exactly once, while preserving the existing assertion that
memory_top_k == 0 skips retrieval.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7f995505-1e81-496b-8358-e03004653723

📥 Commits

Reviewing files that changed from the base of the PR and between 603eeba and 7808476.

📒 Files selected for processing (8)
  • src/engine/bare.rs
  • src/engine/bare/dispatch.rs
  • src/engine/bare/emission.rs
  • src/engine/bare/llm_turn.rs
  • src/engine/bare/model_switch.rs
  • src/engine/bare/tests.rs
  • src/managers.rs
  • src/stream/handler.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/engine/bare/emission.rs
  • src/engine/bare.rs

Comment thread src/engine/bare/llm_turn.rs Outdated
Comment thread src/stream/handler.rs
@bobrykov
bobrykov merged commit 53adf58 into master Aug 6, 2026
8 checks passed
@bobrykov
bobrykov deleted the refactor/bare-loop branch August 6, 2026 07:39
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.

1 participant