Skip to content

MoA: ensembling that improves as mesh nodes join - #1116

Open
michaelneale wants to merge 68 commits into
mainfrom
feat/moa-synthesis
Open

MoA: ensembling that improves as mesh nodes join#1116
michaelneale wants to merge 68 commits into
mainfrom
feat/moa-synthesis

Conversation

@michaelneale

@michaelneale michaelneale commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

ready for review. Every claim below is measured through real open-weight models; see evals/moa-openrouter/RESULTS.md for method, numbers, and withdrawn results.

Closes part of #1115.

What this delivers

MoA rebuilt on Together + Hermes research, tuned for a heterogeneous mesh: the virtual mesh model gets stronger as nodes join, without a weak node ever dragging it down.

  • Reasoning/answer turns → participants draft, refine after seeing each other's drafts (Together's layers), then one aggregator synthesizes.
  • Tool turns → route to the single best tool-caller (voting/advice measured null-to-harmful there).
  • Everything best-effort → bounded waits; slow/absent peers can't stall a turn.

The core result

A pool of ≥2 sufficiently-capable participants beats the single best member of that pool on reasoning turns — and the active ingredient is test-time ensembling, not model diversity:

pool layered vs best member note
2× 32B different (Qwen+Mistral) 49W/24T/6L, p=2e-9 length-clean (MoA shorter, won 93% of shorter trials)
2× 32B same model 48W/23T/2L, p=2e-12 compute-matched to above; Fisher p=0.27 — indistinguishable
4× 8B different 11W/68T/1L, p=0.006 8B needs ~4; 2–3× 8B is null

Two samples of one model do as well as two different models (matches Self-MoA, arXiv:2502.00674). So a same-model mesh should get MoA too.

The three mesh changes

  1. Refine on correlated drafts, not just small pools — the gate keyed on model size; it now keys on draft correlation (homogeneous or all-small). This is what makes a repeated-model pool run the layered path that measured 48/2.
  2. Admission control — a weak worker never joins a pool that has a stronger one. Measured (arm C): admitting an 8B into a 32B pool never helped and raised losses 2→5. Mixed pool → keep big-tier; all-small or all-big → untouched; lone strong model → serves solo.
  3. Same-model self-fill — a mesh where every node serves the same model previously formed no pool at all (dedup collapsed it to 1 worker). It now adds extra nodes serving that model as workers (distinct endpoints only, capped at 2). This is the "add a modest node and it helps" case.

Also fixed (found by measuring the shipped path, not the harness)

Nine eval-vs-production divergences that each erased the gain until fixed (grace finalizing before refinement; reducer/refiner truncation discarding most of each answer; prompts that told workers to "be concise"/"be direct"; named vs anonymous reducer inputs; role-tier token starvation; arbiter shipping one worker's answer instead of synthesizing). Plus a real production panic in mesh-llm-guardrails (byte-slicing on a multi-byte character), with regression tests.

Two hypotheses were tested and rejected by measurement, then reverted — not rationalised.

Protocol / compatibility

No wire, gossip, plugin, or skippy-ABI changes. Actor ranking reads the existing gossiped tool_use capability. New behaviour is in-memory config (ReferencePolicy, RefinementPolicy). One caller-visible change: reasoning_effort/enable_thinking on a model=mesh request is ignored (that override only selected a broken config where reasoning models return content: null).

Validation

cargo test -p mesh-mixture-of-agents   211 pass
cargo test -p mesh-llm-host-runtime   1858 pass
cargo test -p mesh-llm-guardrails       19 pass
clippy -D warnings (moa, host-runtime, guardrails, mesh-llm)   0 errors
cargo fmt --all --check                                        clean

Live studies are #[ignore]d and gated on OPENROUTER_API_KEY; CI skips them.

Honest limits (for the reviewer)

  • All quality numbers are through the eval harness / real models, not a live 2-node mesh — consistent with "MoA on main is tested with data, not mesh nodes". The mesh-config changes (admission, self-fill) are unit-tested; their end-to-end quality lift is inferred from the harness Self-MoA result.
  • Single judge model. Length bias was caught (judge was scoring length, r=+0.68) and controlled; residual r ranges +0.13 to +0.30 across runs, so single-run deltas are directional.
  • Ties dominate at small scale — "stronger" means wins-a-minority, rarely-loses, not uniformly better.
  • The +14.8k line count is almost entirely recorded traces / eval data (real_traces.json, *.jsonl, RESULTS.md); the code surface is 13 files.

Summary by CodeRabbit

  • New Features
    • Added graceful fallback for mesh requests when multi-model routing is unavailable.
    • Improved multi-model responses with optional peer refinement and smarter worker selection.
    • Added specialized handling for tool-enabled requests, including advisory model input.
  • Bug Fixes
    • Prevented incomplete responses from being selected as final answers.
    • Added fallback retries when providers require reasoning parameters.
    • Improved resilience when individual models fail, time out, or return unusable results.
  • Documentation
    • Added evaluation reports, usage guidance, and test fixtures for multi-model behavior.

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Review Change Stack

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

MoA now supports single-model fallback routing, structured worker-pool assembly, truncation-aware text arbitration, optional refinement, asymmetric tool handling, and expanded replay and OpenRouter evaluation coverage.

Changes

MoA routing and worker-pool assembly

Layer / File(s) Summary
Degraded routing
crates/mesh-llm-host-runtime/src/network/openai/ingress.rs, crates/mesh-llm-host-runtime/src/network/openai/moa_gateway/mod.rs
MoA can rewrite model: "mesh" to an advertised real model and continue through ordinary routing. MoA-handled requests still terminate normally.
Worker-pool construction and gateway configuration
crates/mesh-llm-host-runtime/src/network/openai/moa_gateway/pool.rs, crates/mesh-llm-host-runtime/src/network/openai/moa_gateway/workers.rs, crates/mesh-mixture-of-agents/src/worker.rs
The gateway groups aliases, resolves reachable backends, applies tier admission, ranks actor candidates, limits committee size, configures patience profiles, and disables worker thinking.

Mixture-of-agents orchestration

Layer / File(s) Summary
Backend replies and context packing
crates/mesh-mixture-of-agents/src/backend.rs, crates/mesh-mixture-of-agents/src/normalize.rs, crates/mesh-mixture-of-agents/src/context.rs, crates/mesh-mixture-of-agents/src/fanout.rs
Backend replies now preserve truncation state. Text and tool turns use different prompts and payload budgets. Fan-out supports qualification thresholds, refinement-aware grace expiry, and advisory gathering.
Text arbitration and refinement
crates/mesh-mixture-of-agents/src/arbiter.rs, crates/mesh-mixture-of-agents/src/refinement.rs, crates/mesh-mixture-of-agents/src/reducer.rs
Arbitration is text-only. Truncated or disagreeing answers escalate to synthesis. Optional cross-peer refinement uses bounded, best-effort worker rounds and host-provided actor candidates.
Asymmetric tool turns
crates/mesh-mixture-of-agents/src/lib.rs, crates/mesh-mixture-of-agents/src/tool_turn.rs
Tool queries select an actor model and may gather tool-free advisory references. The actor receives real tool schemas and applies forced-tool and failure fallbacks.

Validation and evaluation

Layer / File(s) Summary
Integration and replay validation
crates/mesh-mixture-of-agents/tests/*.rs, crates/mesh-mixture-of-agents/tests/fixtures/*
Tests cover partial worker failure, truncation handling, tool-call preservation, refinement timing, fallback behavior, and gateway configuration defaults.
OpenRouter evaluation tooling
evals/moa-openrouter/*.py, evals/moa-openrouter/*.jsonl, evals/moa-openrouter/README.md, evals/moa-openrouter/RESULTS.md
The PR adds OpenRouter recording, replay-fixture generation, ablation analysis, agentic and corpus traces, evaluation scripts, and documented results.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

  • Mesh-LLM/mesh-llm#820: Modifies overlapping MoA gateway, arbitration, fan-out, context, and worker-selection code.
  • Mesh-LLM/mesh-llm#823: Revises the same MoA gateway and mixture-of-agents routing paths.
  • Mesh-LLM/mesh-llm#837: Changes overlapping gateway, arbitration, fan-out, refinement, and truncation behavior.

Suggested labels: experimental

Suggested reviewers: ndizazzo

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the PR's main change: MoA ensembling that benefits from additional mesh nodes.
✨ 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 feat/moa-synthesis

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.

@github-actions

Copy link
Copy Markdown
Contributor

This pull request is currently a draft. Reviews will not take place until the PR is marked as ready for review.

@michaelneale michaelneale changed the title MoA: give workers tools, synthesize disagreement, survive flaky peers MoA: asymmetric tool turns — best tool-caller acts, references advise Jul 31, 2026
@michaelneale michaelneale changed the title MoA: asymmetric tool turns — best tool-caller acts, references advise MoA: a small-model mesh that beats its best member Aug 2, 2026
@michaelneale michaelneale changed the title MoA: a small-model mesh that beats its best member MoA: refinement-based collaboration for small-model meshes Aug 3, 2026
@michaelneale michaelneale changed the title MoA: refinement-based collaboration for small-model meshes MoA: ensembling that improves as mesh nodes join Aug 4, 2026
@michaelneale
michaelneale marked this pull request as ready for review August 4, 2026 04:14
@github-actions
github-actions Bot requested a review from i386 August 4, 2026 04:15

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 19

🧹 Nitpick comments (18)
evals/moa-openrouter/probe_tools.py (1)

65-70: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Manage the output file handles in both eval scripts. Both scripts open a JSONL output file without a context manager, so no exit path guarantees a close. The shared fix is to scope each handle with with open(...).

  • evals/moa-openrouter/probe_tools.py#L65-L70: move the module-level open("fanout.jsonl", "a") into main and scope it with with, so importing the module no longer creates the file.
  • evals/moa-openrouter/record_agentic.py#L236-L293: replace out = open("agentic.jsonl", "w") plus out.close() with a with open(...) block, so an exception during a scenario still closes the file.
🤖 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 `@evals/moa-openrouter/probe_tools.py` around lines 65 - 70, Manage both output
file handles with context managers: in evals/moa-openrouter/probe_tools.py lines
65-70, move the module-level open call into main and scope it with with; in
evals/moa-openrouter/record_agentic.py lines 236-293, replace the out handle and
explicit close with a with open block covering scenario processing. Ensure
importing probe_tools.py does not create the output file and exceptions still
close both handles.
evals/moa-openrouter/record_agentic.py (1)

275-291: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use iterable unpacking for the message list.

Ruff reports RUF005 here. Replace the list concatenation with unpacking.

♻️ Proposed refactor
-                messages = messages + [
+                messages = [
+                    *messages,
                     {
                         "role": "assistant",
🤖 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 `@evals/moa-openrouter/record_agentic.py` around lines 275 - 291, Update the
message accumulation expression in the agentic recording flow to use iterable
unpacking for the existing messages and the two new message dictionaries,
replacing list concatenation while preserving their order and contents.

Source: Linters/SAST tools

evals/moa-openrouter/make_fixture.py (2)

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

Remove the redundant int call.

round() with a single argument already returns an int. Ruff reports this as RUF046.

♻️ Proposed change
-        "elapsed_ms": int(round((w["elapsed"] or 0) * 1000)),
+        "elapsed_ms": round((w["elapsed"] or 0) * 1000),
🤖 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 `@evals/moa-openrouter/make_fixture.py` at line 23, Update the elapsed_ms
expression in the fixture-building logic to remove the redundant int wrapper and
rely on round((w["elapsed"] or 0) * 1000) returning an integer, preserving the
existing elapsed fallback and rounding behavior.

Source: Linters/SAST tools


14-16: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Resolve the input and output paths relative to the script.

OUT and both open calls use paths relative to the current working directory. The script writes the fixture to the wrong location, or fails, when it runs from the repository root. The docstring does not state the required working directory. The two input handles also stay open until interpreter exit.

Anchor the paths to __file__ and use context managers.

♻️ Proposed change
 import json
+from pathlib import Path
 
-OUT = "../../crates/mesh-mixture-of-agents/tests/fixtures/real_traces.json"
+HERE = Path(__file__).resolve().parent
+OUT = HERE.parents[1] / "crates/mesh-mixture-of-agents/tests/fixtures/real_traces.json"
-for line in open("agentic.jsonl"):
-    line = line.strip()
-    if not line:
-        continue
-    r = json.loads(line)
-    cases.append(
+with open(HERE / "agentic.jsonl") as fh:
+    agentic_lines = fh.read().splitlines()
+for line in agentic_lines:
+    line = line.strip()
+    if not line:
+        continue
+    r = json.loads(line)
+    cases.append(

Apply the same change to the corpus.jsonl loop at line 53.

Also applies to: 34-34, 53-53, 71-72

🤖 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 `@evals/moa-openrouter/make_fixture.py` around lines 14 - 16, Update the
fixture path setup in the script to resolve OUT and both input paths relative to
the script’s __file__, so execution is independent of the current working
directory. Wrap every input and output open call, including the corpus.jsonl
loop, in context managers to close handles promptly.
crates/mesh-mixture-of-agents/tests/eval_openrouter.rs (1)

1453-1459: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Arm C can get one candidate while arm B gets none.

diverse.len().max(1) forces at least one homogeneous candidate. If every diverse peer call fails, arm B runs with zero candidates and is identical to arm A, while arm C still receives one candidate. The reported B-C differential then measures candidate presence, not family diversity. Match the counts exactly.

♻️ Proposed change
-            for _ in 0..diverse.len().max(1) {
+            for _ in 0..diverse.len() {
                 if let Some(c) = structured_proposal(&backend, &finalizer, task).await {
                     homo.push(c);
                 }
             }
🤖 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 `@crates/mesh-mixture-of-agents/tests/eval_openrouter.rs` around lines 1453 -
1459, Update the homogeneous candidate loop in the arm C setup to generate
exactly diverse.len() candidates, removing the max(1) fallback. Preserve the
existing structured_proposal failure handling so zero diverse candidates
produces zero homogeneous candidates and the B-C comparison measures family
diversity.
crates/mesh-llm-host-runtime/src/network/openai/moa_gateway/workers.rs (1)

213-217: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Update the stale comment on enable_thinking.

The comment says try_handle_moa overrides this value "when the caller has expressed a preference". That is no longer true. effective_enable_thinking_for_moa now always returns Some(false) (lines 17-46), and try_handle_moa assigns it unconditionally. A reader following this comment would look for caller-driven behavior that no longer exists.

📝 Proposed wording
-        // Defaults to leaving each model's thinking behavior alone.
-        // `try_handle_moa` overrides this from the inbound request body
-        // when the caller has expressed a preference
-        // (`reasoning_effort: "none"`, `enable_thinking: false`, etc.).
-        enable_thinking: None,
+        // Placeholder: `try_handle_moa` always overwrites this with
+        // `effective_enable_thinking_for_moa`, which is `Some(false)` as a
+        // policy. Caller reasoning knobs are parsed for logging only.
+        enable_thinking: None,
🤖 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 `@crates/mesh-llm-host-runtime/src/network/openai/moa_gateway/workers.rs`
around lines 213 - 217, Update the comment above enable_thinking to reflect that
effective_enable_thinking_for_moa always returns Some(false) and try_handle_moa
assigns that value unconditionally; remove the outdated claim that inbound
caller preferences control the override.
crates/mesh-mixture-of-agents/src/normalize.rs (1)

55-60: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider passing truncation into the normalizer.

Every path in this file hardcodes truncated: false, and each caller must remember to stamp the real value after the call (fanout::gather_workers_incremental, fanout::gather_references, refinement::refine_round). A new call site that omits the stamp silently reports a truncated answer as complete, which is the exact failure this field exists to prevent. Accepting the flag as a parameter of normalize_worker_output would make the compiler enforce it.

🤖 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 `@crates/mesh-mixture-of-agents/src/normalize.rs` around lines 55 - 60, Update
normalize_worker_output to accept a truncation flag parameter and use it when
constructing WorkerOutput instead of hardcoding truncated: false. Pass the
actual flag from every caller, including fanout::gather_workers_incremental,
fanout::gather_references, and refinement::refine_round, then remove their
post-call stamping.
crates/mesh-mixture-of-agents/src/backend.rs (1)

155-159: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

The retry removes caller-supplied chat_template_kwargs content.

apply_enable_thinking merges enable_thinking into any existing chat_template_kwargs object. The retry removes the whole chat_template_kwargs key, so template options the caller set for other purposes are lost on the retried request. Removing only the thinking keys keeps the rest intact.

♻️ Proposed change
                 let mut retry_body = body.clone();
                 if let Some(obj) = retry_body.as_object_mut() {
                     obj.remove("reasoning_effort");
-                    obj.remove("chat_template_kwargs");
+                    if let Some(kwargs) = obj
+                        .get_mut("chat_template_kwargs")
+                        .and_then(Value::as_object_mut)
+                    {
+                        kwargs.remove("enable_thinking");
+                        if kwargs.is_empty() {
+                            obj.remove("chat_template_kwargs");
+                        }
+                    }
                 }
🤖 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 `@crates/mesh-mixture-of-agents/src/backend.rs` around lines 155 - 159, The
retry preparation in the backend request flow must preserve caller-supplied
chat_template_kwargs. Update the retry_body cleanup to remove only the
thinking-related entries added by apply_enable_thinking, while retaining the
rest of the chat_template_kwargs object; keep removal of reasoning_effort
unchanged.
crates/mesh-llm-host-runtime/src/network/openai/moa_gateway/mod.rs (1)

34-42: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Consider the token requirement and a deterministic pick when degrading.

degrade_to_single_model takes the first model returned by models_being_served(). Two effects follow:

  • The picked model may not satisfy required_tokens. build_moa_config applies context_selection::context_can_satisfy per worker, but this fallback path applies no context check, so a request that needs a large context can be routed to a small-context model.
  • The iteration order of models_being_served() decides the answer. If that order is not stable, the same lone node can answer with different models across requests.

A deterministic selection that prefers locally served models with sufficient context would make the degraded path predictable.

🤖 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 `@crates/mesh-llm-host-runtime/src/network/openai/moa_gateway/mod.rs` around
lines 34 - 42, Update the fallback model selection around
degrade_to_single_model to filter out the virtual model and retain only models
whose context satisfies required_tokens, preferring locally served models when
available. Sort or otherwise select from the remaining candidates
deterministically before choosing the target, while preserving the existing 503
response when no eligible model exists.
crates/mesh-mixture-of-agents/src/fanout.rs (1)

350-363: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Simplify the reference stop condition.

Line 352 checks !outputs.is_empty() && outputs.len() >= min_references. The first clause only matters when min_references == 0, and the only caller passes dispatched.len().div_ceil(2).max(1). Either drop the redundant clause or clamp min_references with .max(1) inside this function so the intent is local.

🤖 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 `@crates/mesh-mixture-of-agents/src/fanout.rs` around lines 350 - 363, In the
reference collection loop, simplify the stop condition around outputs.len() and
min_references: remove the redundant outputs non-empty check, or normalize
min_references to at least one within the function before the loop. Preserve the
behavior that collection stops once the configured minimum number of references
is reached.
crates/mesh-llm-host-runtime/src/network/openai/ingress.rs (1)

842-857: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Set the degraded flag in the match arm instead of comparing model values.

Line 856 infers degraded from routing_model != decision.effective_model. The branch that produced the value is already known at line 847. The comparison holds today only because degrade_to_single_model excludes moa::VIRTUAL_MODEL_NAME, so the rewritten name can never equal "mesh". If that exclusion changes, the pipeline classifier would run against a stale decision. A flag set in the arm removes the dependency.

♻️ Proposed refactor
-    let mut routing_model = decision.effective_model.clone();
-    let tcp_stream = match try_handle_moa_intercept(tcp_stream, &mut request, &ctx, &decision).await
-    {
-        MoaInterceptResult::Handled => return,
-        MoaInterceptResult::NotMoa(stream) => stream,
-        MoaInterceptResult::Degraded { stream, model } => {
-            routing_model = model;
-            stream
-        }
-    };
-
-    let mut tcp_stream = tcp_stream;
-    // A degraded turn is a plain single-model request; skip the pipeline
-    // classifier (computed against "mesh") and route it directly.
-    let degraded = routing_model != decision.effective_model;
+    let mut routing_model = decision.effective_model.clone();
+    let mut degraded = false;
+    let mut tcp_stream =
+        match try_handle_moa_intercept(tcp_stream, &mut request, &ctx, &decision).await {
+            MoaInterceptResult::Handled => return,
+            MoaInterceptResult::NotMoa(stream) => stream,
+            MoaInterceptResult::Degraded { stream, model } => {
+                routing_model = model;
+                degraded = true;
+                stream
+            }
+        };
+
+    // A degraded turn is a plain single-model request; skip the pipeline
+    // classifier (computed against "mesh") and route it directly.
🤖 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 `@crates/mesh-llm-host-runtime/src/network/openai/ingress.rs` around lines 842
- 857, Update the request-routing flow around try_handle_moa_intercept to
initialize a degraded flag before the match and set it explicitly in the
MoaInterceptResult::Degraded arm. Replace the routing_model !=
decision.effective_model comparison with that flag, while leaving the Handled
and NotMoa paths unchanged so pipeline classification is skipped whenever the
intercept produced a degraded single-model request.
crates/mesh-mixture-of-agents/src/context.rs (1)

532-545: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Name the advisor payload bound.

reducer_payload_budget and REFINEMENT_DRAFT_BUDGET express their limits as named items with the reasoning attached. This loop hardcodes 500 and the derived 497. Introduce a constant, for example ADVICE_PAYLOAD_BUDGET, and derive the truncation length from it so the two values cannot drift.

♻️ Proposed refactor
+/// How much of each advisor's prose the actor may see.
+const ADVICE_PAYLOAD_BUDGET: usize = 500;
+
@@
-            let payload = if r.payload.len() > 500 {
-                format!("{}...", crate::worker::truncate_chars(&r.payload, 497))
+            let payload = if r.payload.len() > ADVICE_PAYLOAD_BUDGET {
+                format!(
+                    "{}...",
+                    crate::worker::truncate_chars(&r.payload, ADVICE_PAYLOAD_BUDGET - 3)
+                )
             } else {
                 r.payload.clone()
             };
🤖 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 `@crates/mesh-mixture-of-agents/src/context.rs` around lines 532 - 545, In the
advisor payload loop over references, replace the hardcoded 500-byte/character
bound and derived 497 truncation length with a named constant such as
ADVICE_PAYLOAD_BUDGET, documenting the limit’s purpose consistently with
reducer_payload_budget and REFINEMENT_DRAFT_BUDGET. Derive the truncation length
from that constant so the displayed payload plus ellipsis always stays within
the same budget.
crates/mesh-mixture-of-agents/src/tool_turn.rs (2)

255-336: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add tests for actor_body.

The policy tests are thorough. actor_body is untested, and it encodes the response contract for the whole tool path: a ToolProposal must emit tool_calls, an Uncertainty with a forced tool must emit the forced call, and an Uncertainty with no forced tool and no references must emit MOA_ERR_NO_USABLE_ANSWER. It is a pure function over WorkerOutput, so it needs no backend. Add cases for those three branches.

🤖 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 `@crates/mesh-mixture-of-agents/src/tool_turn.rs` around lines 255 - 336, Add
unit tests for the pure actor_body function covering three response-contract
branches: ToolProposal produces tool_calls, Uncertainty with a forced tool
produces that forced call, and Uncertainty without a forced tool or references
returns MOA_ERR_NO_USABLE_ANSWER. Construct WorkerOutput fixtures directly
without backend setup, and keep the existing policy tests unchanged.

143-147: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Both new fan-out modules rebuild an identical prompt inside the per-worker loop. dispatch_and_gather_references and refine_round each call a context packer once per spawned worker with arguments that do not vary by worker, so the same PackedContext is built N times. Hoist the packing above the loop and clone the messages into each task.

  • crates/mesh-mixture-of-agents/src/tool_turn.rs#L143-L147: move context::pack_for_reference(session, REFERENCE_HISTORY_MESSAGES) above the for a in &assignments loop and clone packed.messages per task.
  • crates/mesh-mixture-of-agents/src/refinement.rs#L100-L102: move context::pack_for_refinement(session, &texts) above the loop; this call also concatenates and truncates every draft, so the repeated work is larger here.
🤖 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 `@crates/mesh-mixture-of-agents/src/tool_turn.rs` around lines 143 - 147, The
fan-out paths rebuild identical packed context for every worker. In
crates/mesh-mixture-of-agents/src/tool_turn.rs lines 143-147, update
dispatch_and_gather_references to call context::pack_for_reference once before
the for a in &assignments loop and clone packed.messages into each task; in
crates/mesh-mixture-of-agents/src/refinement.rs lines 100-102, update
refine_round to call context::pack_for_refinement once before its worker loop
and clone the resulting messages per task.
crates/mesh-mixture-of-agents/src/refinement.rs (2)

194-291: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a refine_round test for the shortfall path.

The tests cover policy and budget arithmetic well. They do not exercise refine_round, whose central safety claim is that fewer than MIN_DRAFTS refinements return None so the caller keeps the round-1 outputs. reducer.rs already has a FakeBackend helper that can drive this. Add one test where a single worker succeeds and assert refine_round returns None.

🤖 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 `@crates/mesh-mixture-of-agents/src/refinement.rs` around lines 194 - 291, Add
a unit test for refine_round using the existing reducer.rs FakeBackend helper,
configuring one worker to succeed and produce fewer than MIN_DRAFTS refinement
results. Assert that refine_round returns None, preserving the caller’s round-1
outputs; keep the test focused on this shortfall path.

141-148: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Record a summary when a refinement reply is empty.

Line 142 skips a reply whose text is blank and continues without pushing a WorkerSummary. The worker ran and consumed tokens, but it appears in no accounting. Every other outcome in this loop produces a summary, and the round-1 gather reconciles all dispatched workers. Add a failed summary so refinement attempts stay observable.

♻️ Proposed change
                     Ok((model, role, Ok(reply), elapsed)) => {
                         if reply.text.trim().is_empty() {
+                            summaries.push(WorkerSummary {
+                                model,
+                                role,
+                                succeeded: false,
+                                elapsed_ms: elapsed,
+                                output_kind: None,
+                                confidence: None,
+                            });
                             continue;
                         }
🤖 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 `@crates/mesh-mixture-of-agents/src/refinement.rs` around lines 141 - 148,
Update the empty-reply branch in the refinement result loop before continue so
it pushes a failed WorkerSummary for the completed worker, preserving the worker
identity and relevant timing/token accounting; then continue without normalizing
output. Ensure every dispatched refinement attempt remains represented
consistently with the other result branches and round-1 reconciliation.
crates/mesh-mixture-of-agents/src/lib.rs (2)

1309-1309: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Consider extracting the response builders into their own module.

These six builders are now pub(crate) because tool_turn consumes them. They form a separable responsibility: OpenAI wire-response construction. lib.rs is over 2,000 lines and this PR modifies it.

Move best_answer, fallback_worker_response, tool_proposal_response, error_response, chat_response, and tool_call_response plus response_builder_tests into a response module. That keeps lib.rs shrinking as the crate grows and moves the tests with the behavior.

As per coding guidelines: "When modifying a Rust source file over 1,000 lines, extract any separable responsibility into a semantically named module, keep the new file under 1,000 lines, and move relevant tests with the extracted behavior."

Also applies to: 1326-1326, 1338-1338, 1374-1374, 1420-1420, 1434-1434

🤖 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 `@crates/mesh-mixture-of-agents/src/lib.rs` at line 1309, Extract the OpenAI
wire-response builders best_answer, fallback_worker_response,
tool_proposal_response, error_response, chat_response, and tool_call_response
from lib.rs into a semantically named response module, preserving their
pub(crate) visibility and existing behavior. Move response_builder_tests into
the same module, update module declarations and call sites such as tool_turn,
and keep the new module under 1,000 lines.

Source: Coding guidelines


316-350: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Simplify the now-unreachable tool bookkeeping on the text path.

The guard at line 310 returns for every turn where has_tools is true or forced_tool is set. After that guard, query_uses_tools at line 343 is always false, selected_tool_names is always empty, and grace_mode_for_turn(session, has_tools) is always called with has_tools == false, so GraceMode::Tool is unreachable here. The comment at lines 318-342 explains a decision that this path no longer makes.

Replace the computed values with constants and move the historical rationale to tool_turn. The change keeps behavior identical and removes a misleading contract for the next reader.

♻️ Proposed simplification
-    let query_uses_tools = forced_tool.is_some() || has_tools;
-    let selected_tool_names = if let Some(tool) = forced_tool {
-        vec![tool.name.clone()]
-    } else if query_uses_tools {
-        selected_tool_names_for_turn(session, allowed_tools)
-    } else {
-        Vec::new()
-    };
+    // Tool-bearing turns returned above, so this path is always tool-free.
+    let query_uses_tools = false;
+    let selected_tool_names: Vec<String> = Vec::new();
🤖 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 `@crates/mesh-mixture-of-agents/src/lib.rs` around lines 316 - 350, In the
post-guard text path, replace the computed tool state with constant no-tools
values and call grace_mode_for_turn with tools unavailable, since forced_tool
and has_tools cannot reach this branch. Move the historical tool-availability
rationale from this block to tool_turn, retaining only comments relevant to the
text path. Preserve the existing behavior while removing the misleading
query_uses_tools and selected_tool_names contract.
🤖 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 `@crates/mesh-llm-host-runtime/src/network/openai/moa_gateway/workers.rs`:
- Around line 399-470: Extract the separable pool-assembly responsibilities from
this oversized module into semantically named modules, such as admission and
patience, keeping each new file under 1,000 lines. Move apply_admission_control
and its tests into the admission module; move cap_committee,
self_fill_from_extra_instances, assemble_worker_pool, and their tests into an
appropriate pool/committee module, and move PatienceProfile, patience_profile,
and related tests into the patience module. Update module declarations and call
sites while preserving existing behavior.
- Around line 522-548: Update assemble_worker_pool and
self_fill_from_extra_instances to carry the initial remote peer selected by
add_worker_backend, and skip that peer when adding extra instances to the MoA
committee. Pass the tracked peer through the call chain, remove the unused _http
parameter from self_fill_from_extra_instances and its callers, and preserve the
existing worker-count limit.

In `@crates/mesh-mixture-of-agents/src/arbiter.rs`:
- Around line 455-463: Update the match in single_output_decision so
OutputKind::ToolProposal returns Decision::NeedsReducer with the appropriate
reducer reason, alongside OutputKind::Uncertainty. Keep non-tool, non-uncertain
outputs on the existing Decision::Answer path.

In `@crates/mesh-mixture-of-agents/src/backend.rs`:
- Around line 356-361: Update the tool-proposal path around
BackendReply::complete to validate the raw args as JSON and set the reply’s
truncated state when parsing fails specifically with finish_reason == "length";
preserve the complete status for valid JSON or non-length finish reasons, and
avoid treating the unwrap_or("{}") fallback as proof that the original arguments
were complete.

In `@crates/mesh-mixture-of-agents/src/context.rs`:
- Around line 347-453: Extract the packing responsibilities from context.rs into
semantically named modules: move REFERENCE_PREAMBLE and pack_for_reference with
their tests to a reference module, pack_for_refinement and
REFINEMENT_DRAFT_BUDGET with related tests to refinement_context, and
pack_for_actor with its tests to actor. Update module declarations, imports, and
call sites so the existing behavior and APIs remain unchanged while keeping each
source file under 1,000 lines.
- Around line 475-497: Verify the refinement configuration in the code and
measurement setup around the PackedContext construction and its max_tokens
value. If the measured refinement configuration used the full worker token
budget rather than 1024 tokens, increase max_tokens to that budget so synthesis
is not unnecessarily truncated; otherwise preserve the current value.

In `@crates/mesh-mixture-of-agents/src/fanout.rs`:
- Around line 383-397: Update reconcile_dispatched, used by gather_references
and gather_workers_incremental, to match returned summaries to dispatched
workers by slot index or another unique per-dispatch identifier instead of model
name. Propagate that identifier through the dispatch/result flow so duplicate
same-model slots reconcile independently and failed or cancelled slots remain in
TurnResult.worker_summaries.

In `@crates/mesh-mixture-of-agents/src/reducer.rs`:
- Around line 154-158: Make reducer truncation observable in the output handling
around the result mapping that returns `(name, result.map(|reply| reply.text))`.
Preserve the full reducer text, but inspect `reply.truncated` and log when the
final synthesis response was truncated, using the existing logging mechanism and
context available in the reducer flow.

In `@crates/mesh-mixture-of-agents/src/tool_turn.rs`:
- Around line 178-187: Cap the optional advisory wait in the tool-turn flow
before calling gather_references, using the same worker-budget fraction
established by refinement.rs rather than the full config.worker_timeout. Keep
min_references unchanged and preserve pack_for_actor’s empty-reference fallback
when the bounded advisory round expires.
- Around line 79-92: Update the TurnResult construction in finalize_actor_output
so reducer_used reflects actor_ok instead of always being true. Preserve the
existing reducer_attempts value and response handling, ensuring failed reducer
attempts that return fallback_worker_response or error_response report
reducer_used as false.
- Around line 54-55: Update handle_tool_query to accept and propagate the
caller’s has_tools flag, passing it to context::pack_for_actor instead of
hardcoding true. Ensure tool_proposal_response and forced tool_call_response
paths use the same flag so tool-call responses are omitted when the request has
no tools array.

In `@crates/mesh-mixture-of-agents/src/worker.rs`:
- Around line 144-181: Align the documentation for canonical_base_name and
pool_is_homogeneous with the actual behavior: quant-tagged names remain distinct
from untagged names, so mixed quant variants are not considered homogeneous and
refinement::refinement_expected will not enable refinement for them. Update the
comments to remove the claim that quant variants share a base, and add coverage
for a plain name versus a quant-tagged alias to pin this contract.

In `@crates/mesh-mixture-of-agents/tests/eval_openrouter.rs`:
- Around line 2212-2215: Update the solo-baseline setup around small_mesh_pool
so it does not claim pool[0] is the strongest member: either select the
strongest model explicitly or revise the comment and output wording to describe
the first declaration-order entry. Also handle an empty pool before indexing,
using the test’s existing failure or early-return convention instead of allowing
pool[0] to panic.
- Around line 460-466: Replace the local truncate helper and its call sites with
the exported truncate_chars behavior, ensuring truncation never slices through a
UTF-8 character. Update crates/mesh-mixture-of-agents/src/lib.rs::truncate_chars
if necessary so its limit semantics match the required character-count boundary,
and apply the safe helper to model output, HTTP error bodies, and advisor
errors.

In `@crates/mesh-mixture-of-agents/tests/sim_real_traces.rs`:
- Around line 541-557: Update
crates/mesh-mixture-of-agents/tests/sim_real_traces.rs#L541-L557 to remove the
result.reducer_used skip for tool-bearing turns so the majority-argument
assertions execute; update is_reducer_call at
crates/mesh-mixture-of-agents/tests/sim_real_traces.rs#L146-L154 to recognize
the pack_for_actor system marker “## Advice from other models” and return
SYNTHESIZED for the actor call instead of replaying a worker body.

In `@crates/mesh-mixture-of-agents/tests/sim_refinement_mesh_conditions.rs`:
- Around line 3-7: Update the module-level rationale near the refinement
description to remove the withdrawn “42/66/12, p=5.2e-05” result or replace it
with the controlled evaluation result from evals/moa-openrouter/RESULTS.md;
retain the explanation that refinement is best-effort and may introduce another
hanging fan-out.

In `@evals/moa-openrouter/probe_tools.py`:
- Around line 100-105: Update the tool-call formatting comprehension in the
result-printing block to read arguments defensively via the nested function
mapping, defaulting missing or falsy values to "{}" as in record_agentic.py,
while preserving the existing call-name formatting and probe flow.

In `@evals/moa-openrouter/record.py`:
- Around line 31-41: Update the worker record construction around the response
handling to persist the complete OpenAI response, including usage and unselected
fields, by storing resp directly in each record while retaining the existing
derived fields as needed.

In `@evals/moa-openrouter/RESULTS.md`:
- Around line 487-493: Update the documented merge-blocker status around the
harness-versus-production parity claim in moa::handle_turn: either provide
evidence that the orchestration gap is resolved, or explicitly record an
approved waiver. Do not present the production reasoning-quality improvement as
validated while this blocker remains unresolved.

---

Nitpick comments:
In `@crates/mesh-llm-host-runtime/src/network/openai/ingress.rs`:
- Around line 842-857: Update the request-routing flow around
try_handle_moa_intercept to initialize a degraded flag before the match and set
it explicitly in the MoaInterceptResult::Degraded arm. Replace the routing_model
!= decision.effective_model comparison with that flag, while leaving the Handled
and NotMoa paths unchanged so pipeline classification is skipped whenever the
intercept produced a degraded single-model request.

In `@crates/mesh-llm-host-runtime/src/network/openai/moa_gateway/mod.rs`:
- Around line 34-42: Update the fallback model selection around
degrade_to_single_model to filter out the virtual model and retain only models
whose context satisfies required_tokens, preferring locally served models when
available. Sort or otherwise select from the remaining candidates
deterministically before choosing the target, while preserving the existing 503
response when no eligible model exists.

In `@crates/mesh-llm-host-runtime/src/network/openai/moa_gateway/workers.rs`:
- Around line 213-217: Update the comment above enable_thinking to reflect that
effective_enable_thinking_for_moa always returns Some(false) and try_handle_moa
assigns that value unconditionally; remove the outdated claim that inbound
caller preferences control the override.

In `@crates/mesh-mixture-of-agents/src/backend.rs`:
- Around line 155-159: The retry preparation in the backend request flow must
preserve caller-supplied chat_template_kwargs. Update the retry_body cleanup to
remove only the thinking-related entries added by apply_enable_thinking, while
retaining the rest of the chat_template_kwargs object; keep removal of
reasoning_effort unchanged.

In `@crates/mesh-mixture-of-agents/src/context.rs`:
- Around line 532-545: In the advisor payload loop over references, replace the
hardcoded 500-byte/character bound and derived 497 truncation length with a
named constant such as ADVICE_PAYLOAD_BUDGET, documenting the limit’s purpose
consistently with reducer_payload_budget and REFINEMENT_DRAFT_BUDGET. Derive the
truncation length from that constant so the displayed payload plus ellipsis
always stays within the same budget.

In `@crates/mesh-mixture-of-agents/src/fanout.rs`:
- Around line 350-363: In the reference collection loop, simplify the stop
condition around outputs.len() and min_references: remove the redundant outputs
non-empty check, or normalize min_references to at least one within the function
before the loop. Preserve the behavior that collection stops once the configured
minimum number of references is reached.

In `@crates/mesh-mixture-of-agents/src/lib.rs`:
- Line 1309: Extract the OpenAI wire-response builders best_answer,
fallback_worker_response, tool_proposal_response, error_response, chat_response,
and tool_call_response from lib.rs into a semantically named response module,
preserving their pub(crate) visibility and existing behavior. Move
response_builder_tests into the same module, update module declarations and call
sites such as tool_turn, and keep the new module under 1,000 lines.
- Around line 316-350: In the post-guard text path, replace the computed tool
state with constant no-tools values and call grace_mode_for_turn with tools
unavailable, since forced_tool and has_tools cannot reach this branch. Move the
historical tool-availability rationale from this block to tool_turn, retaining
only comments relevant to the text path. Preserve the existing behavior while
removing the misleading query_uses_tools and selected_tool_names contract.

In `@crates/mesh-mixture-of-agents/src/normalize.rs`:
- Around line 55-60: Update normalize_worker_output to accept a truncation flag
parameter and use it when constructing WorkerOutput instead of hardcoding
truncated: false. Pass the actual flag from every caller, including
fanout::gather_workers_incremental, fanout::gather_references, and
refinement::refine_round, then remove their post-call stamping.

In `@crates/mesh-mixture-of-agents/src/refinement.rs`:
- Around line 194-291: Add a unit test for refine_round using the existing
reducer.rs FakeBackend helper, configuring one worker to succeed and produce
fewer than MIN_DRAFTS refinement results. Assert that refine_round returns None,
preserving the caller’s round-1 outputs; keep the test focused on this shortfall
path.
- Around line 141-148: Update the empty-reply branch in the refinement result
loop before continue so it pushes a failed WorkerSummary for the completed
worker, preserving the worker identity and relevant timing/token accounting;
then continue without normalizing output. Ensure every dispatched refinement
attempt remains represented consistently with the other result branches and
round-1 reconciliation.

In `@crates/mesh-mixture-of-agents/src/tool_turn.rs`:
- Around line 255-336: Add unit tests for the pure actor_body function covering
three response-contract branches: ToolProposal produces tool_calls, Uncertainty
with a forced tool produces that forced call, and Uncertainty without a forced
tool or references returns MOA_ERR_NO_USABLE_ANSWER. Construct WorkerOutput
fixtures directly without backend setup, and keep the existing policy tests
unchanged.
- Around line 143-147: The fan-out paths rebuild identical packed context for
every worker. In crates/mesh-mixture-of-agents/src/tool_turn.rs lines 143-147,
update dispatch_and_gather_references to call context::pack_for_reference once
before the for a in &assignments loop and clone packed.messages into each task;
in crates/mesh-mixture-of-agents/src/refinement.rs lines 100-102, update
refine_round to call context::pack_for_refinement once before its worker loop
and clone the resulting messages per task.

In `@crates/mesh-mixture-of-agents/tests/eval_openrouter.rs`:
- Around line 1453-1459: Update the homogeneous candidate loop in the arm C
setup to generate exactly diverse.len() candidates, removing the max(1)
fallback. Preserve the existing structured_proposal failure handling so zero
diverse candidates produces zero homogeneous candidates and the B-C comparison
measures family diversity.

In `@evals/moa-openrouter/make_fixture.py`:
- Line 23: Update the elapsed_ms expression in the fixture-building logic to
remove the redundant int wrapper and rely on round((w["elapsed"] or 0) * 1000)
returning an integer, preserving the existing elapsed fallback and rounding
behavior.
- Around line 14-16: Update the fixture path setup in the script to resolve OUT
and both input paths relative to the script’s __file__, so execution is
independent of the current working directory. Wrap every input and output open
call, including the corpus.jsonl loop, in context managers to close handles
promptly.

In `@evals/moa-openrouter/probe_tools.py`:
- Around line 65-70: Manage both output file handles with context managers: in
evals/moa-openrouter/probe_tools.py lines 65-70, move the module-level open call
into main and scope it with with; in evals/moa-openrouter/record_agentic.py
lines 236-293, replace the out handle and explicit close with a with open block
covering scenario processing. Ensure importing probe_tools.py does not create
the output file and exceptions still close both handles.

In `@evals/moa-openrouter/record_agentic.py`:
- Around line 275-291: Update the message accumulation expression in the agentic
recording flow to use iterable unpacking for the existing messages and the two
new message dictionaries, replacing list concatenation while preserving their
order and contents.
🪄 Autofix (Beta)

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: 1f1bf079-4ba5-464a-8bc2-9d17fd49c867

📥 Commits

Reviewing files that changed from the base of the PR and between 66c40d6 and 228a1ef.

📒 Files selected for processing (39)
  • crates/mesh-llm-guardrails/src/rescue.rs
  • crates/mesh-llm-host-runtime/src/network/openai/ingress.rs
  • crates/mesh-llm-host-runtime/src/network/openai/moa_gateway/mod.rs
  • crates/mesh-llm-host-runtime/src/network/openai/moa_gateway/workers.rs
  • crates/mesh-mixture-of-agents/src/arbiter.rs
  • crates/mesh-mixture-of-agents/src/backend.rs
  • crates/mesh-mixture-of-agents/src/context.rs
  • crates/mesh-mixture-of-agents/src/fanout.rs
  • crates/mesh-mixture-of-agents/src/lib.rs
  • crates/mesh-mixture-of-agents/src/normalize.rs
  • crates/mesh-mixture-of-agents/src/reducer.rs
  • crates/mesh-mixture-of-agents/src/refinement.rs
  • crates/mesh-mixture-of-agents/src/tool_guard.rs
  • crates/mesh-mixture-of-agents/src/tool_turn.rs
  • crates/mesh-mixture-of-agents/src/worker.rs
  • crates/mesh-mixture-of-agents/tests/eval_openrouter.rs
  • crates/mesh-mixture-of-agents/tests/fixtures/ablation_tasks.json
  • crates/mesh-mixture-of-agents/tests/fixtures/committee_tasks.json
  • crates/mesh-mixture-of-agents/tests/fixtures/real_traces.json
  • crates/mesh-mixture-of-agents/tests/sim_all_workers_fail.rs
  • crates/mesh-mixture-of-agents/tests/sim_enable_thinking_propagation.rs
  • crates/mesh-mixture-of-agents/tests/sim_partial_worker_survival.rs
  • crates/mesh-mixture-of-agents/tests/sim_real_traces.rs
  • crates/mesh-mixture-of-agents/tests/sim_refinement_mesh_conditions.rs
  • crates/mesh-mixture-of-agents/tests/sim_strong_patience.rs
  • crates/mesh-mixture-of-agents/tests/sim_tool_call_text_not_passed_as_content.rs
  • crates/mesh-mixture-of-agents/tests/sim_tool_result_routes_to_reducer.rs
  • crates/mesh-mixture-of-agents/tests/sim_worker_accounting.rs
  • evals/moa-openrouter/README.md
  • evals/moa-openrouter/RESULTS.md
  • evals/moa-openrouter/agentic.jsonl
  • evals/moa-openrouter/analyze_ablation.py
  • evals/moa-openrouter/corpus.jsonl
  • evals/moa-openrouter/fanout.jsonl
  • evals/moa-openrouter/make_fixture.py
  • evals/moa-openrouter/orclient.py
  • evals/moa-openrouter/probe_tools.py
  • evals/moa-openrouter/record.py
  • evals/moa-openrouter/record_agentic.py

Comment thread crates/mesh-llm-host-runtime/src/network/openai/moa_gateway/workers.rs Outdated
Comment thread crates/mesh-llm-host-runtime/src/network/openai/moa_gateway/workers.rs Outdated
Comment on lines +455 to 463
// Tool turns take the asymmetric actor path, so this text-path arbiter
// never sees an executable ToolProposal (tool-shaped text is demoted to
// Uncertainty by `enforce_tool_call_contract` when tools are disabled).
match output.kind {
OutputKind::ToolProposal if has_tools => {
if let Some(ref name) = output.tool_name {
Decision::ToolCall {
name: name.clone(),
arguments: output
.tool_arguments
.clone()
.unwrap_or(Value::Object(Default::default())),
}
} else {
Decision::Answer(output.payload.clone())
}
}
OutputKind::Uncertainty => Decision::NeedsReducer {
reason: "single worker uncertain".into(),
},
_ => Decision::Answer(output.payload.clone()),
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm empty-allow-list behaviour of enforce_tool_call_contract.
fd -t f 'tool_guard.rs' crates -x sed -n '1,80p'

Repository: Mesh-LLM/mesh-llm

Length of output: 2911


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate and inspect the relevant arbiter implementation and nearby tests without running repo code.
fd -t f 'arbiter.rs' crates/mesh-mixture-of-agents/src -x sh -c '
  echo "=== $1 ==="
  wc -l "$1"
  echo "--- outline ---"
  ast-grep outline "$1" || true
  echo "--- relevant sections ---"
  sed -n "420,490p" "$1"
  echo "--- empty_allowed_list references in arbiter ---"
  rg -n "enforce_tool_call_contract|single_output_decision|arbitrate|ToolProposal|OutputKind::" "$1" || true
' sh {}

echo "=== tool_guard tests ==="
fd -t f 'tool_guard.rs' crates -x sh -c '
  sed -n "60,160p" "$1"
' sh {}

Repository: Mesh-LLM/mesh-llm

Length of output: 13581


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the multi-output branch and make sure ToolProposal is absent from answer/critique/uncertainty collections.
fd -t f 'arbiter.rs' crates/mesh-mixture-of-agents/src -x sh -c '
  echo "=== $1 relevant branch ==="
  sed -n "33,75p" "$1"
  echo "--- ToolProposal/Arbitrate tests ---"
  rg -n "ToolProposal|empty_allowed_list_is_noop|enforce_tool_call_contract|arbitrate" "$1" || true
' sh {}

# Read-only behavioral probe of the relevant guard condition and decision arms from the source text.
python3 - <<'PY'
from pathlib import Path
arbiter = Path("crates/mesh-mixture-of-agents/src/arbiter.rs").read_text()
for pattern in [
    "let allowed_tools = enforce_tool_call_contract",
    "let mut allowed_tools = allowed_tools.clone()",
    "enforce_tool_call_contract(&mut out, allowed_tools",
]:
    print(f"{pattern!r}: {'present' if pattern in arbiter else 'absent'}")
print("single_output_decision guard references allowed_tools:", "allowed_tools" in arbiter[arbiter.find("fn single_output_decision"):arbiter.find("\n}", arbiter.find("fn single_output_decision"))])
PY

Repository: Mesh-LLM/mesh-llm

Length of output: 2795


Route lone ToolProposal outputs to the reducer.

enforce_tool_call_contract leaves an empty allow-list unchanged, and this arbiter has no allowed_tools guard. A single tool-shaped payload reaches single_output_decision as OutputKind::ToolProposal and falls into the _ arm as Decision::Answer(payload), returning proposal prose to a tool-free request. Send ToolProposal to NeedsReducer so synthesis produces real text.

🐛 Proposed fix
-    // Tool turns take the asymmetric actor path, so this text-path arbiter
-    // never sees an executable ToolProposal (tool-shaped text is demoted to
-    // Uncertainty by `enforce_tool_call_contract` when tools are disabled).
+    // Tool turns take the asymmetric actor path, so a `ToolProposal` here is
+    // tool-shaped text on a tools-disabled turn. Its payload is not an answer,
+    // so synthesize instead of returning the proposal prose verbatim.
     match output.kind {
-        OutputKind::Uncertainty => Decision::NeedsReducer {
+        OutputKind::Uncertainty => Decision::NeedsReducer {
             reason: "single worker uncertain".into(),
         },
+        OutputKind::ToolProposal => Decision::NeedsReducer {
+            reason: "single worker proposed a tool on a tool-free turn".into(),
+        },
         _ => Decision::Answer(output.payload.clone()),
     }
📝 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
// Tool turns take the asymmetric actor path, so this text-path arbiter
// never sees an executable ToolProposal (tool-shaped text is demoted to
// Uncertainty by `enforce_tool_call_contract` when tools are disabled).
match output.kind {
OutputKind::ToolProposal if has_tools => {
if let Some(ref name) = output.tool_name {
Decision::ToolCall {
name: name.clone(),
arguments: output
.tool_arguments
.clone()
.unwrap_or(Value::Object(Default::default())),
}
} else {
Decision::Answer(output.payload.clone())
}
}
OutputKind::Uncertainty => Decision::NeedsReducer {
reason: "single worker uncertain".into(),
},
_ => Decision::Answer(output.payload.clone()),
}
// Tool turns take the asymmetric actor path, so a `ToolProposal` here is
// tool-shaped text on a tools-disabled turn. Its payload is not an answer,
// so synthesize instead of returning the proposal prose verbatim.
match output.kind {
OutputKind::Uncertainty => Decision::NeedsReducer {
reason: "single worker uncertain".into(),
},
OutputKind::ToolProposal => Decision::NeedsReducer {
reason: "single worker proposed a tool on a tool-free turn".into(),
},
_ => Decision::Answer(output.payload.clone()),
}
🤖 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 `@crates/mesh-mixture-of-agents/src/arbiter.rs` around lines 455 - 463, Update
the match in single_output_decision so OutputKind::ToolProposal returns
Decision::NeedsReducer with the appropriate reducer reason, alongside
OutputKind::Uncertainty. Keep non-tool, non-uncertain outputs on the existing
Decision::Answer path.

Comment thread crates/mesh-mixture-of-agents/src/backend.rs
Comment thread crates/mesh-mixture-of-agents/src/context.rs
Comment on lines +541 to +557
// The reducer may legitimately rewrite the turn into prose; only
// assert when a tool call was emitted from worker proposals.
if tools.is_empty() || result.reducer_used {
continue;
}

let majority_val: Value = serde_json::from_str(&majority).unwrap_or(Value::Null);
for (name, args) in &tools {
let got: Value = serde_json::from_str(args).unwrap_or(Value::Null);
assert_eq!(
got, majority_val,
"case `{}`: tool `{name}` should use the majority arguments \
({majority_n} workers proposed {majority}), not a minority variant. \
Got {args}",
case.id
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

The replay harness still models the symmetric reducer path for tool turns. Tool-bearing turns now route to handle_tool_query, which packs the final request with pack_for_actor and always reports reducer_used: true. Both sites assume the old reducer fan-out shape, so the tool assertions no longer measure the shipped path.

  • crates/mesh-mixture-of-agents/tests/sim_real_traces.rs#L541-L557: replace the result.reducer_used skip, which is always true for has_tools cases and makes the majority-argument assert_eq! unreachable.
  • crates/mesh-mixture-of-agents/tests/sim_real_traces.rs#L146-L154: extend is_reducer_call to recognize the pack_for_actor system marker (## Advice from other models), so the actor call is answered with SYNTHESIZED instead of a replayed worker body.
📍 Affects 1 file
  • crates/mesh-mixture-of-agents/tests/sim_real_traces.rs#L541-L557 (this comment)
  • crates/mesh-mixture-of-agents/tests/sim_real_traces.rs#L146-L154
🤖 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 `@crates/mesh-mixture-of-agents/tests/sim_real_traces.rs` around lines 541 -
557, Update crates/mesh-mixture-of-agents/tests/sim_real_traces.rs#L541-L557 to
remove the result.reducer_used skip for tool-bearing turns so the
majority-argument assertions execute; update is_reducer_call at
crates/mesh-mixture-of-agents/tests/sim_real_traces.rs#L146-L154 to recognize
the pack_for_actor system marker “## Advice from other models” and return
SYNTHESIZED for the actor call instead of replaying a worker body.

Comment thread evals/moa-openrouter/probe_tools.py
Comment on lines +31 to +41
text, tcs = oc.first_choice(resp)
ch = (resp.get("choices") or [{}])[0]
return {
"model": model,
"tier": oc.tier(model),
"elapsed": round(elapsed, 2),
"error": resp.get("error"),
"finish_reason": ch.get("finish_reason"),
"text": text,
"tool_calls": tcs,
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Persist the complete worker response.

The recorder discards usage and all unselected OpenAI response fields. This conflicts with evals/moa-openrouter/README.md, which requires full response preservation, including usage. Store resp in each worker record, or add the omitted fields and narrow the documented contract.

Proposed fix
         return {
             "model": model,
             "tier": oc.tier(model),
             "elapsed": round(elapsed, 2),
+            "response": resp,
             "error": resp.get("error"),
             "finish_reason": ch.get("finish_reason"),
             "text": text,
             "tool_calls": tcs,
         }
📝 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
text, tcs = oc.first_choice(resp)
ch = (resp.get("choices") or [{}])[0]
return {
"model": model,
"tier": oc.tier(model),
"elapsed": round(elapsed, 2),
"error": resp.get("error"),
"finish_reason": ch.get("finish_reason"),
"text": text,
"tool_calls": tcs,
}
text, tcs = oc.first_choice(resp)
ch = (resp.get("choices") or [{}])[0]
return {
"model": model,
"tier": oc.tier(model),
"elapsed": round(elapsed, 2),
"response": resp,
"error": resp.get("error"),
"finish_reason": ch.get("finish_reason"),
"text": text,
"tool_calls": tcs,
}
🤖 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 `@evals/moa-openrouter/record.py` around lines 31 - 41, Update the worker
record construction around the response handling to persist the complete OpenAI
response, including usage and unselected fields, by storing resp directly in
each record while retaining the existing derived fields as needed.

Comment thread evals/moa-openrouter/RESULTS.md

@i386 i386 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

One functional issue found in the committee-cap path.

Comment thread crates/mesh-llm-host-runtime/src/network/openai/moa_gateway/workers.rs Outdated

@i386 i386 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

A second issue concerns the capacity signal that gates admission.

Comment thread crates/mesh-mixture-of-agents/src/worker.rs

i386 commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

Follow-up on the tiering signal: parsing 1B9B from a served name is brittle. Aliases/fine-tunes need not include a size, model names can include unrelated numbers, and the current fallback treats every unparseable name as big. That makes a destructive admission decision from an unverified label.

The better source is GGUF inspection: enumerate the GGUF tensors and sum their element_count to obtain the exact total stored-parameter count, then gossip that value through the existing ServedModelMetadata.parameter_count_b. Use it for MoA tiering when present; retain name parsing only as a lower-confidence fallback, and represent missing data as Unknown rather than big. For MoE we may eventually also want a distinct active-parameter signal, but total tensor count is already a materially better safety gate than the name heuristic.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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 current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/mesh-llm-host-runtime/src/network/openai/moa_gateway/pool.rs`:
- Around line 242-244: Update assemble_worker_pool and
self_fill_from_extra_instances to retain and pass the resolved alias group and
required_tokens. Query all aliases in that group when self-filling, and apply
the same remote-host context eligibility check as add_worker_backend before
adding each endpoint, so insufficient-context hosts are excluded. Add
regressions covering equivalent canonical aliases and hosts below the required
context capacity.
- Around line 19-29: Update resolve_one_worker_from_aliases to accept and use
the existing WorkerBackendResolution value for immutable resolution inputs,
removing its separate targets, aliases, and required_tokens parameters as
appropriate. Remove the unused local_count parameter and eliminate the
#[allow(clippy::too_many_arguments)] suppression, updating all call sites to
match.
🪄 Autofix (Beta)

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: ba7e182b-4708-4854-986a-b213df4ea9cb

📥 Commits

Reviewing files that changed from the base of the PR and between 228a1ef and 365c470.

📒 Files selected for processing (3)
  • crates/mesh-llm-host-runtime/src/network/openai/moa_gateway/mod.rs
  • crates/mesh-llm-host-runtime/src/network/openai/moa_gateway/pool.rs
  • crates/mesh-llm-host-runtime/src/network/openai/moa_gateway/workers.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/mesh-llm-host-runtime/src/network/openai/moa_gateway/mod.rs

Comment thread crates/mesh-llm-host-runtime/src/network/openai/moa_gateway/pool.rs
Comment thread crates/mesh-llm-host-runtime/src/network/openai/moa_gateway/pool.rs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🤖 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 `@crates/mesh-llm-host-runtime/src/network/openai/moa_gateway/pool.rs`:
- Around line 314-333: Ensure self-fill never duplicates an existing peer or
re-adds the local backend. Update add_worker_backend to return or store the
selected remote mesh::PeerId, collect peer identities already represented by the
current backends, and filter hosts_for_model(&name) in the self-fill loop before
creating each RemoteModelBackend; use the actual peer-id type from
RemoteModelBackend.

In `@evals/moa-openrouter/analyze_ablation.py`:
- Around line 175-196: Update the verdict logic in the analysis flow after the
B/C estimates to bootstrap a paired B−C differential using the same task
resamples, producing its own confidence interval. Base the “gain is CONTENT”
classification on that differential interval being entirely above zero, rather
than on b_lo; retain the existing B−A verdict and non-content-specific
comparison separately.

In `@evals/moa-openrouter/orclient.py`:
- Around line 74-76: Update the HTTPError handling in chat to decode the
response body defensively, falling back safely when the body is not valid UTF-8
so UnicodeDecodeError cannot escape the handler. Preserve the existing HTTP
status/detail formatting and structured error return behavior.

In `@evals/moa-openrouter/probe_tools.py`:
- Around line 27-31: Update each function schema in probe_tools.py, including
the schemas around the shown properties and the additional locations noted in
the review, to set additionalProperties to false alongside type, properties, and
required. Ensure every probe tool schema matches the production constraint
enforced by tool_guard.
- Line 79: Update the oc.chat call in the probe flow, including the
corresponding calls around the additionally affected lines, to pass
no_think=True. Preserve the existing model, messages, tools, token, and
temperature arguments so all workers and aggregators use the production MoA
thinking configuration.
- Line 199: Update the fanout_tool_result record call to store the complete
tool-result conversation by passing msgs_b as messages alongside the existing
results payload. Preserve the current record name and result data.

In `@evals/moa-openrouter/record_agentic.py`:
- Around line 131-140: Update consensus_tool to normalize valid JSON arguments
before counting tool-call proposals: parse each argument string, re-serialize
parsed objects with stable key ordering, and use that canonical form in the
consensus key. Preserve malformed arguments under a separate raw-string fallback
key, while keeping tool names and existing consensus selection behavior
unchanged.
🪄 Autofix (Beta)

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: 80edd6cb-badd-48c4-9245-c592678da89a

📥 Commits

Reviewing files that changed from the base of the PR and between 365c470 and 01d717d.

📒 Files selected for processing (40)
  • crates/mesh-llm-guardrails/src/rescue.rs
  • crates/mesh-llm-host-runtime/src/network/openai/ingress.rs
  • crates/mesh-llm-host-runtime/src/network/openai/moa_gateway/mod.rs
  • crates/mesh-llm-host-runtime/src/network/openai/moa_gateway/pool.rs
  • crates/mesh-llm-host-runtime/src/network/openai/moa_gateway/workers.rs
  • crates/mesh-mixture-of-agents/src/arbiter.rs
  • crates/mesh-mixture-of-agents/src/backend.rs
  • crates/mesh-mixture-of-agents/src/context.rs
  • crates/mesh-mixture-of-agents/src/fanout.rs
  • crates/mesh-mixture-of-agents/src/lib.rs
  • crates/mesh-mixture-of-agents/src/normalize.rs
  • crates/mesh-mixture-of-agents/src/reducer.rs
  • crates/mesh-mixture-of-agents/src/refinement.rs
  • crates/mesh-mixture-of-agents/src/tool_guard.rs
  • crates/mesh-mixture-of-agents/src/tool_turn.rs
  • crates/mesh-mixture-of-agents/src/worker.rs
  • crates/mesh-mixture-of-agents/tests/eval_openrouter.rs
  • crates/mesh-mixture-of-agents/tests/fixtures/ablation_tasks.json
  • crates/mesh-mixture-of-agents/tests/fixtures/committee_tasks.json
  • crates/mesh-mixture-of-agents/tests/fixtures/real_traces.json
  • crates/mesh-mixture-of-agents/tests/sim_all_workers_fail.rs
  • crates/mesh-mixture-of-agents/tests/sim_enable_thinking_propagation.rs
  • crates/mesh-mixture-of-agents/tests/sim_partial_worker_survival.rs
  • crates/mesh-mixture-of-agents/tests/sim_real_traces.rs
  • crates/mesh-mixture-of-agents/tests/sim_refinement_mesh_conditions.rs
  • crates/mesh-mixture-of-agents/tests/sim_strong_patience.rs
  • crates/mesh-mixture-of-agents/tests/sim_tool_call_text_not_passed_as_content.rs
  • crates/mesh-mixture-of-agents/tests/sim_tool_result_routes_to_reducer.rs
  • crates/mesh-mixture-of-agents/tests/sim_worker_accounting.rs
  • evals/moa-openrouter/README.md
  • evals/moa-openrouter/RESULTS.md
  • evals/moa-openrouter/agentic.jsonl
  • evals/moa-openrouter/analyze_ablation.py
  • evals/moa-openrouter/corpus.jsonl
  • evals/moa-openrouter/fanout.jsonl
  • evals/moa-openrouter/make_fixture.py
  • evals/moa-openrouter/orclient.py
  • evals/moa-openrouter/probe_tools.py
  • evals/moa-openrouter/record.py
  • evals/moa-openrouter/record_agentic.py
🚧 Files skipped from review as they are similar to previous changes (31)
  • crates/mesh-mixture-of-agents/tests/sim_strong_patience.rs
  • crates/mesh-mixture-of-agents/src/tool_guard.rs
  • crates/mesh-mixture-of-agents/tests/sim_tool_call_text_not_passed_as_content.rs
  • crates/mesh-mixture-of-agents/src/reducer.rs
  • crates/mesh-llm-host-runtime/src/network/openai/moa_gateway/mod.rs
  • evals/moa-openrouter/fanout.jsonl
  • crates/mesh-mixture-of-agents/tests/sim_tool_result_routes_to_reducer.rs
  • crates/mesh-mixture-of-agents/tests/sim_all_workers_fail.rs
  • crates/mesh-mixture-of-agents/src/normalize.rs
  • crates/mesh-mixture-of-agents/tests/sim_partial_worker_survival.rs
  • crates/mesh-mixture-of-agents/tests/fixtures/committee_tasks.json
  • crates/mesh-mixture-of-agents/tests/sim_enable_thinking_propagation.rs
  • crates/mesh-mixture-of-agents/tests/fixtures/ablation_tasks.json
  • crates/mesh-mixture-of-agents/src/worker.rs
  • crates/mesh-mixture-of-agents/src/backend.rs
  • crates/mesh-mixture-of-agents/tests/sim_worker_accounting.rs
  • crates/mesh-llm-host-runtime/src/network/openai/ingress.rs
  • crates/mesh-mixture-of-agents/tests/fixtures/real_traces.json
  • evals/moa-openrouter/agentic.jsonl
  • crates/mesh-mixture-of-agents/tests/sim_real_traces.rs
  • crates/mesh-mixture-of-agents/src/refinement.rs
  • evals/moa-openrouter/corpus.jsonl
  • crates/mesh-mixture-of-agents/tests/sim_refinement_mesh_conditions.rs
  • crates/mesh-mixture-of-agents/src/tool_turn.rs
  • crates/mesh-llm-host-runtime/src/network/openai/moa_gateway/workers.rs
  • crates/mesh-mixture-of-agents/src/context.rs
  • crates/mesh-llm-guardrails/src/rescue.rs
  • crates/mesh-mixture-of-agents/tests/eval_openrouter.rs
  • crates/mesh-mixture-of-agents/src/lib.rs
  • crates/mesh-mixture-of-agents/src/fanout.rs
  • crates/mesh-mixture-of-agents/src/arbiter.rs

Comment thread crates/mesh-llm-host-runtime/src/network/openai/moa_gateway/pool.rs
Comment on lines +175 to +196
b_pt, b_k = point_estimate(tasks, "B")
c_pt, c_k = point_estimate(tasks, "C")
b_lo, b_hi = bootstrap_ci(tasks, "B", args.iters, args.seed)
c_lo, c_hi = bootstrap_ci(tasks, "C", args.iters, args.seed)

print(" net uplift = P(rescue) - P(harm), equal-weight mean over tasks")
print(f" B (real) uplift {b_pt:+.3f} 95% CI [{b_lo:+.3f}, {b_hi:+.3f}] (tasks n={b_k})")
print(f" C (shuffled) uplift {c_pt:+.3f} 95% CI [{c_lo:+.3f}, {c_hi:+.3f}] (tasks n={c_k})")
print(f" differential B-C: {b_pt - c_pt:+.3f} (content effect beyond token/prompt effect)")
print()

# Verdicts (directional; the CI is what matters for a claim).
if b_lo > 0:
print(" => references HELP: B net uplift CI is entirely > 0")
elif b_hi < 0:
print(" => references HARM: B net uplift CI is entirely < 0")
else:
print(" => inconclusive: B net uplift CI spans 0")
if b_pt - c_pt > 0 and b_lo > 0:
print(" => and the gain is CONTENT (B > C), not just extra tokens/prompt")
elif abs(b_pt - c_pt) < 0.02:
print(" => gain (if any) is NOT content-specific (B ~ C)")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Bootstrap the B−C differential before classifying a content effect.

Line 193 tests b_lo > 0, but b_lo is the lower confidence bound for B−A. It is not a confidence bound for B−C. This condition can report a content-specific gain when B−C is statistically uncertain.

Use the same paired task and draw resamples to calculate a B−C confidence interval. Base the content verdict on that interval.

🤖 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 `@evals/moa-openrouter/analyze_ablation.py` around lines 175 - 196, Update the
verdict logic in the analysis flow after the B/C estimates to bootstrap a paired
B−C differential using the same task resamples, producing its own confidence
interval. Base the “gain is CONTENT” classification on that differential
interval being entirely above zero, rather than on b_lo; retain the existing B−A
verdict and non-content-specific comparison separately.

Comment thread evals/moa-openrouter/orclient.py
Comment thread evals/moa-openrouter/probe_tools.py
"""

def one(model):
resp, elapsed = oc.chat(model, messages, tools=tools, max_tokens=max_tokens, temperature=0.8)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Use the production MoA thinking setting.

oc.chat defaults no_think to False. Its contract states that no_think=True mirrors the MoA default. These calls can therefore measure reasoning-enabled workers and aggregators instead of the production configuration. Pass no_think=True; orclient.chat already retries without the flag for providers that require reasoning.

Proposed fix
-        resp, elapsed = oc.chat(model, messages, tools=tools, max_tokens=max_tokens, temperature=0.8)
+        resp, elapsed = oc.chat(
+            model, messages, tools=tools, max_tokens=max_tokens, temperature=0.8, no_think=True
+        )
...
-        aggregator, msgs, tools=TOOLS if with_tools else None, max_tokens=384, temperature=0.3
+        aggregator, msgs, tools=TOOLS if with_tools else None,
+        max_tokens=384, temperature=0.3, no_think=True

Also applies to: 135-137

🤖 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 `@evals/moa-openrouter/probe_tools.py` at line 79, Update the oc.chat call in
the probe flow, including the corresponding calls around the additionally
affected lines, to pass no_think=True. Preserve the existing model, messages,
tools, token, and temperature arguments so all workers and aggregators use the
production MoA thinking configuration.

},
]
res_b = summarize(fan_out(msgs_b, TOOLS), "B. TOOL-RESULT TURN (agentic step 2)")
record("fanout_tool_result", results=res_b)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Record the tool-result input conversation.

This record contains only results. It omits the user prompt, assistant tool call, tool-call ID, and tool result in msgs_b. A replay corpus cannot reconstruct this tool-result turn. Store messages=msgs_b with the result.

Proposed fix
-    record("fanout_tool_result", results=res_b)
+    record("fanout_tool_result", messages=msgs_b, results=res_b)
📝 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
record("fanout_tool_result", results=res_b)
record("fanout_tool_result", messages=msgs_b, results=res_b)
🤖 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 `@evals/moa-openrouter/probe_tools.py` at line 199, Update the
fanout_tool_result record call to store the complete tool-result conversation by
passing msgs_b as messages alongside the existing results payload. Preserve the
current record name and result data.

Comment on lines +131 to +140
def consensus_tool(workers):
"""Most-proposed (name, arguments) across workers, or None."""
counts = {}
for w in workers:
for c in w.get("tool_calls") or []:
key = (c["function"]["name"], c["function"].get("arguments") or "{}")
counts[key] = counts.get(key, 0) + 1
if not counts:
return None
return max(counts.items(), key=lambda kv: kv[1])[0]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Normalize JSON arguments before counting tool-call consensus.

Equivalent argument objects can use different key order or whitespace. The current raw-string key splits those votes. A lower-support proposal can then control the canned observation and all later recorded steps.

Parse valid argument JSON and serialize it with stable key ordering before counting proposals. Keep malformed arguments in a separate raw fallback key.

🤖 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 `@evals/moa-openrouter/record_agentic.py` around lines 131 - 140, Update
consensus_tool to normalize valid JSON arguments before counting tool-call
proposals: parse each argument string, re-serialize parsed objects with stable
key ordering, and use that canonical form in the consensus key. Preserve
malformed arguments under a separate raw-string fallback key, while keeping tool
names and existing consensus selection behavior unchanged.

…laky peers

Agentic MoA turns now reliably produce tool calls, and diverging answers are
synthesized instead of relaying one arbitrarily-chosen worker's text.

Fixes found by replaying recorded traces from 9 open-weight models:

* Tools were withheld from workers. `query_uses_tools` was derived from
  `looks_like_tool_intent`, an English keyword match on the user's text. "The
  test suite is failing. Find out which test fails and why" matches nothing, so
  workers were dispatched without tool schemas and the arbiter ran with
  has_tools=false — a unanimous tool proposal then fell through to the answer
  path and leaked "calling search" as prose. 5 of 10 recorded scenarios hit
  this. Tool availability is now the caller's declaration.

* Argument outliers could win a unanimous tool call. Native tool calls are all
  normalized to a fixed 0.9 confidence, so the confidence-only tiebreak could
  not separate 8 workers proposing {"path":"src"} from one hallucinating
  {"path":"rust_project/src"}. Added argument clustering (key-order
  independent); the largest cluster wins and argument-free calls never outvote
  filled-in ones.

* Early exit committed on tied arguments. Early exit runs on whoever has
  arrived, so two fast models agreeing on a tool name with different arguments
  was a coin flip that also aborted the workers who would have broken the tie.
  It now waits.

* Truncation was invisible. `finish_reason` was never read; 39/140 recorded
  responses came back "length" and 24 carried partial text, which parsed as a
  normal answer and could be returned verbatim. Plumbed through as
  BackendReply.truncated -> WorkerOutput.truncated; such answers are barred
  from consensus and verbatim output but still feed synthesis, labelled
  incomplete.

* A strict endpoint could kill a worker permanently. minimax-m2.5 returns
  HTTP 400 "Reasoning is mandatory for this endpoint" and failed 12/12
  requests; HttpBackend now drops the thinking flags and retries once.

Policy changes:

* Thinking is always off for MoA workers, not merely defaulted off. The
  previous escape hatch only let callers request the broken configuration:
  qwen3-32b spent 408 reasoning tokens against a 384-token cap and returned
  null content. Ignored overrides are logged.

* Diverging answers go to the reducer. Returning the top-confidence answer was
  near-arbitrary because models rarely emit our confidence envelope, so
  everything defaults to 0.5 and max_by returns whichever worker finished
  first. Agreement still short-circuits without paying for synthesis.

* Reducer prompt adopts Together's aggregator framing (synthesize, critically
  evaluate, agreement is not proof), keeping our per-worker attribution,
  structured tool proposals, and 500-char payload bound.

Tests: new sim_real_traces.rs replays 56 recorded cases from 9 models through
handle_turn, plus regression unit tests for each bug. Recorders and corpora
live in evals/moa-openrouter/.
…r acts

Tool-bearing turns now use the Hermes/Nous-style asymmetric shape instead of a
majority vote across workers:

- references run TOOL-FREE and only advise in prose
- the single best tool-caller (the "actor") acts on that advice with the real
  tools and emits the tool call

Tool authority now tracks capability, not popularity. The old path let several
weak models proposing a popular-but-wrong tool outvote the one strong
tool-caller that picked correctly (observed: qwen3-32b alone chose run_command
for a failing-test triage while the smaller models chose list_dir, and the vote
shipped list_dir). An actor model removes that failure class instead of patching
the vote arithmetic.

Stays a pure stateless /v1/chat/completions turn — references are regenerated
from the caller's transcript each request, and the external client still owns
tool execution. Text-only turns are unchanged (symmetric fan-out +
synthesis-on-divergence).

Actor selection is capability-first: the host ranks candidates by gossiped
`tool_use` level (Supported > Likely > None), then model size tier, then stable
order, and passes the ordering to the engine via the new
`GatewayConfig.actor_candidates`. Empty ordering falls back to the engine's
name-derived size tier, so existing callers and tests are unaffected.

Mesh guardrails for mixed/public meshes: references are gathered with a bounded
wait (proceed at a majority of advisors, never block on the slow tail), and the
actor is called through the existing hedged ladder so a slow/broken best
candidate falls through to the next tool-capable peer.

New:
- crates/mesh-mixture-of-agents/src/tool_turn.rs — asymmetric tool-turn handler
- context::pack_for_actor — "advise, then act" framing (vs synthesis framing)
- fanout::gather_references — bounded, tool-free reference collection
- GatewayConfig.actor_candidates + reducer_candidates honours it
- host compute_actor_candidates from gossiped tool_use + size

The dead majority-vote code in the arbiter (now unreachable — tool turns bypass
it) is removed in the follow-up commit.

Tests: eval_openrouter.rs adds a live OpenRouter harness (ignored by default)
that runs the real handle_turn over 6 open-weight models with mesh-realism
latency/failure injection, for benchmarking asymmetric MoA vs single models.
192 engine tests pass; clippy -D warnings and fmt clean on both crates.
Tool turns now take the asymmetric actor path (previous commit), so the
arbiter's tool-proposal voting is unreachable — and it *was* the
majority-of-weakness bug: a popular-but-wrong tool choice from several weak
models could outvote the one strong tool-caller that picked correctly.

Removes it rather than leaving dead code:

- delete best_tool_proposal, best_tool_proposal_by_consensus, and
  decisive_argument_cluster
- drop the tool-arbitration and tool-vs-answer branches from arbitrate, and
  the tool-consensus branch from try_early_decision
- drop the now-unused has_tools parameter from arbitrate, try_early_decision,
  single_output_decision, and gather_workers_incremental
- the arbiter is now purely an answer/critique/uncertainty arbiter; tool-shaped
  text on the text path is already demoted to Uncertainty by
  enforce_tool_call_contract (tools disabled ⇒ empty allow-list)
- drop the arbiter tests that pinned the removed tool-vote behavior; keep and
  re-point every answer-consensus, truncation, synthesis, and tier-gate test

No behavior change for text turns. 178 engine tests pass; host-runtime lib
tests (1851) pass; clippy -D warnings and fmt clean on
mesh-mixture-of-agents, mesh-llm-host-runtime, and mesh-llm.
Replaces the confounded "MoA vs best single model" comparison with a
within-actor ablation that isolates the one variable that matters: the
references. One pinned actor, identical sampling / token budget / system
prompt across all arms; only the advice changes:

  A. actor alone (no references)
  B. actor + real references (the production tool path)
  C. actor + shuffled references (advice from a different task, length-similar)

Metric: rescue (A✗→B✓) minus harm (A✓→B✗). Arm C is the key control — it
separates "useful information" from "extra tokens + a think-carefully prompt".

Why the change: the earlier best-single comparison ran solo models at different
sampling (0.8/512) than the actor (0.3/2048) and with a different system
prompt, and scored transient 429/502/504 and tool-unsupported endpoints as
capability failures. Those numbers measured sampling+prompt+infra, not the
design, and are not citable. This ablation removes all of that by construction:
the same scorer hits all three arms of the same actor, so an imperfect label
largely cancels in the rescue-minus-harm delta.

Also:
- chat_completion_retrying: predeclared retry on transient infra errors, which
  are then excluded (∅) from the capability analysis rather than scored as
  wrong answers.
- MOA_ABLATION_ACTOR / MOA_ABLATION_DRAWS env overrides.
- the old best-single test is kept but documented as a confounded smoke test,
  not evidence.

Pilot findings (5 draws, 4 tasks, ignored/live):
- Strong actor (qwen3-32b): 20/20 all arms — aces every task alone, so
  references are inert-but-harmless (no headroom to measure rescue).
- Weak actor (qwen3-8b): A 15/20, B 20/20, C 15/20 — net uplift +5, all on the
  one task with headroom (triage). Real advice rescued the weak actor every
  draw; SHUFFLED advice did not (C=A), so the gain is advice CONTENT, not token
  count or the decision prompt. Zero harm.

Reading: reference value tracks actor-alone headroom — a safety net when the
actor is weak, dead weight (not damage) when strong. Suggests gating the
reference phase on actor strength. This is a directional pilot, not the
merge-blocking study (that needs ~40 preregistered stratified tasks, k>=10
draws, a paired hierarchical bootstrap CI, and the production-selected actor).
…yzer

The defensible version of the pilot, committed BEFORE running so the labels
are preregistered and can't be seen to chase results.

- tests/fixtures/ablation_tasks.json: 40 preregistered tasks, 4 strata x 10
  (inspect / search / execute / no_tool). Set-valued accept labels
  (accept_tools is a SET; empty = "no tool call"), with optional arg substring
  constraints — addresses the brittle single-tool label from the pilot.
- ablation_scaled_study: loads the fixture, runs A/B/C arms at k draws
  (default 10) with bounded concurrency, writes one JSONL trial per
  (draw,task,arm) to MOA_ABLATION_OUT. Live measurement only; no stats here.
- evals/moa-openrouter/analyze_ablation.py: deterministic paired HIERARCHICAL
  bootstrap — resample tasks within stratum, then draws within task — for a
  95% CI on net uplift = P(rescue) - P(harm), plus the shuffled-arm control
  (content effect = B_uplift - C_uplift). Validated on synthetic data: 3/40
  rescue tasks -> +0.075 point estimate, shuffled +0.000, and the CI honestly
  spans 0 when rescue is sparse (no over-claiming).

Arms (one pinned actor, identical sampling/prompt; only references vary):
  A actor alone · B actor + real references · C actor + shuffled references.

Env: MOA_ABLATION_ACTOR, MOA_ABLATION_DRAWS, MOA_ABLATION_CONCURRENCY,
MOA_ABLATION_OUT. Ignored by default (live network + cost).
Cut the essay-length doc/inline comments added with the actor design down to the
non-obvious "why". No code change.

- tool_turn.rs: 26-line module preamble -> 6 lines; inline comments tightened
  (20% -> 10% comment density)
- context.rs: pack_for_actor docstring 16 -> 5 lines; reducer synthesis-framing
  block 14 -> 6 lines
- workers.rs: compute_actor_candidates docstring 21 -> 6 lines (kept the terse
  sort-key comments, which are load-bearing)
Two more ablation arms to answer "can peers help tool selection at all?",
reusing the A/B/C JSONL schema + analyze_ablation.py.

matched_peer_structured_study — do similar-strength, different-family peers
help a fixed finalizer via STRUCTURED candidate tool calls (not prose)?
  A solo · B diverse (2 different-family peers) · C homogeneous (2 resamples
  of the finalizer's own model). B-C isolates cross-family diversity from
  extra sampling; also records oracle-union.

correction_rescues_weak_tool_caller — the mesh scenario neither Hermes nor
Together handles: a weak tool-caller with no strong peer. Tests correction of
the CONCRETE drafted call instead of pre-hoc advice.
  A draft-alone · B deterministic (schema-validate + re-prompt on structural
  failure) · C semantic (different-family critic reviews the concrete call,
  finalizer revises once).

Findings (live, directional): every tool-selection intervention was
inert-to-harmful vs routing to a capable model. Pre-hoc structured proposals
were flat (37-39/40 all arms). Deterministic correction fired ~never
(qwen3-8b already drafts structurally valid calls ~95%); the residual failures
are semantic (wrong tool choice), which neither validation nor a strong critic
fixed because the revision still runs through the weak actor. Conclusion: tool
selection is a "best capable model acts" task; MoA's value is on the
answer/chat path (untested).
Tests where MoA's value should live per Together's validated claim: open-ended
answer QUALITY on realistic agent-session turns (reason-over-tool-output,
planning, explanation) — not tool selection.

Fixed aggregator; only its input varies:
  A alone · B committee (synthesize 3 diverse-peer drafts, 1 round)
  C layered (peers refine seeing each other first, then synthesize — Together's
  `layers`)

Judged pairwise by an out-of-pool different-family judge (gpt-4o-mini),
position-swapped (win only if consistent both orders), output lengths logged.

- tests/fixtures/committee_tasks.json: 15 realistic reasoning/answer turns
- committee_beats_solo_on_reasoning: writes per-trial JSONL

Pilot findings (2 draws, live; DIRECTIONAL, underpowered):
- committee(B) vs solo(A): win 6 / tie 2 / loss 2 — positive but sign-test
  p=0.29, NOT significant at n=10. Every B win was also the longer answer, so
  length is not cleanly ruled out.
- layered(C) vs committee(B): loss 6 / tie 2 / win 2 — Together's extra
  refinement round is negative value on these tasks, at extra cost.
- 20/30 trials skipped because the aggregator (qwen3-32b) returned empty
  content (reasoning-budget exhaustion — the content:null bug Hermes'
  troubleshooting.md documents). A flaky aggregator degrades the committee;
  the instrument needs empty-output handling before a real run.

Net: first directionally-positive result for MoA in this investigation, but
unproven. Contrast with 5 tool-selection experiments that were all
null-to-harmful. Supports the task-split: route tools to one caller, convene a
(single-round) committee on reasoning turns.
…l transcript

Our references were packed very differently from Hermes', and it was measurably
costly. Adds `pack_for_reference`, which gives advisors only the conversation's
user/assistant prose:

- strips the agent system prompt (an advisor told "you are a coding agent, run
  the tests" role-plays the actor instead of advising it)
- strips the tool transcript (prior tool_calls + results anchored every advisor
  on the trajectory already taken, collapsing the error-independence that makes
  aggregation worth anything)
- drops the "respond with your best answer or tool call" instruction: advisors
  hold no schemas, so asking for a tool call yields tool-shaped prose — exactly
  the advice that pulled the actor off its own better choice
- uniform view across advisors (no per-role trimming), so the packing is a
  stable function of history and caches across iterations
- caps advisor output at 600 tokens (advisor generation dominates turn latency;
  the turn waits for the slowest advisor)

Head-to-head on the same preregistered study (strong actor qwen3-32b, 40 tasks
x 10 draws, identical everything except packing):

  packing      B pass    net uplift      95% CI
  original     359/400   -0.102          [-0.170, -0.045]
  hermes       385/400   -0.037          [-0.090, -0.003]

Harm cut by ~64%. Per-category: search -14 -> -2, execute -25 -> -13,
inspect -2 -> 0. The content-specific component (B-C differential) fell from
-0.075 to -0.015, i.e. with correct packing the residual harm is no longer
mostly "bad advice content".

Honest read: most of the harm I previously attributed to "references" was an
artifact of how WE packed them, not a property of reference-based MoA. The
direction still stands though — even correctly packed, references remain a
small but statistically real regression for a STRONG actor on tool selection
(CI still entirely below zero).

Adds 4 unit tests pinning the packing contract (no tool-call request, no system
prompt leak, no tool-transcript leak, prose preserved), and
MOA_REFERENCE_PACKING=hermes to select the style in the eval harness.
…t failed

Writes up every live study in one place (evals/moa-openrouter/RESULTS.md) so
the conclusions and their caveats survive the investigation.

Headline 2x2 (40 preregistered tasks x 10 draws, paired hierarchical bootstrap):

  actor    packing    B pass     net uplift   95% CI
  strong   original   359/400    -0.102       [-0.170, -0.045]  <- the bug
  strong   hermes     385/400    -0.037       [-0.090, -0.003]
  weak     original   365/400    -0.013       [-0.090, +0.070]
  weak     hermes     377/400    +0.017       [-0.053, +0.100]

Two monotonic effects: fixing the packing helps in both actor conditions, and
references are worth more to a weaker actor. The only significant cell is the
original-packing strong-actor harm — i.e. our bug, not a property of MoA.

Per-stratum (weak + hermes) shows references help exactly where the actor has
headroom (search +10, execute +4) and hurt where it was already perfect
(inspect -7). That is a gating signal.

Also records what did NOT work for tool selection (pre-hoc structured
proposals: flat; deterministic correction: never fires, the weak actor already
emits valid calls ~95%; semantic correction: negative, since the revision still
runs through the weak actor), and the committee pilot on reasoning turns
(directionally positive, n=10, not significant; Together's layering loses to
single-round).
The Hermes-style packing was measured and unit-tested but only wired into the
eval harness — the production tool path still used pack_for_worker_selected,
the exact packing measured at -0.102 net uplift.

tool_turn now calls pack_for_reference: conversation prose only, no agent
system prompt, no tool transcript, no request for a tool call. Same head-to-head
(strong actor, 40 tasks x 10 draws) puts this at -0.037 vs -0.102, and it is the
only configuration where references show a positive point estimate for a weak
actor (+0.017). See evals/moa-openrouter/RESULTS.md.
…no-upside

Arm C (32B x2 + one 8B, weak node admitted) vs arm B (32B x2):

  B  48W 23T 2L  p=2e-12  decided winrate 96%
  C  50W 25T 5L  p=2e-10  decided winrate 91%

Fisher B-vs-C p=0.44 (not separable at n=80) but one-way: admitting the weak
node never helped and raised losses 2 -> 5. Both still beat solo, so a weak
node doesn't collapse the pool, but it adds cost for no upside and a small tail
risk. This is the measured basis for tier-based apply_admission_control.
You asked the right question: we showed admitting a weak node HURTS when the
pool already has two strong (arm C), but never tested where admitting HELPS.

The missing cell — one strong + one weak — is decisive and overturns the
first-cut rule:

  32B + 8B layered vs solo 32B:  47W 27T 5L  p=1.3e-9  length-clean (r=-0.01)

Admission control rejected the 8B here, collapsing the pool to a solo 32B. But
the mixed committee beats solo decisively, so rejecting it threw away MoA in
exactly the core "modest node joins a strong node and helps" case.

Rule corrected: drop small-tier workers only when >=2 big-tier remain (a real
committee survives). If dropping would collapse to solo, keep the mix.

  32B x2 + 8B  -> drop 8B (committee survives; 8B adds nothing, arm C)
  32B + 8B     -> keep both (dropping = solo, and the mix wins 47/5)

Two admission tests updated/added to pin both cases. 69 moa_gateway tests pass;
clippy -D warnings and fmt clean.
Before: a `model=mesh` request on a lone node (or a mesh with <2 workers)
returned 503 "MoA requires ≥2 models". That makes `mesh` unusable as a default
model — it only worked once a committee could form.

Now: when `build_moa_config` can't form a committee but a real model is
available, `try_handle_moa` rewrites `model=mesh` to that model and hands the
stream back to normal single-model routing. `mesh` works everywhere —
passthrough on one node, committee once a second worker joins. Only a node with
no model at all still 503s.

Both call sites handle the new fall-through:
 - passive/transport path already routes a returned `Some(stream)` normally;
 - host/ingress path: `try_handle_moa_intercept` gained a `Degraded { stream,
   model }` result. The pre-computed `decision.effective_model` is stale
   ("mesh") after degradation, so the degraded model name is threaded into
   `route_request`, and the pipeline classifier (computed against "mesh") is
   skipped for degraded turns.

Unchanged from main was the hard 503; this is the missing piece for `mesh` as a
superior always-on drop-in. 1859 host tests pass; clippy -D warnings and fmt
clean.
Fan-out cost is ~2N+1 model calls per turn (N drafts + N refines + 1
synthesis), and measured quality is flat past ~4 workers while latency and
spend keep climbing. On a big shared mesh — say 20 nodes — an uncapped pool
fanned out to all of them: 41 calls for no quality gain.

`cap_committee` now trims the assembled pool to the best MAX_COMMITTEE_WORKERS
(4) by the same capability ranking used for actor selection (gossiped tool_use,
then size, then stable index). Nodes beyond the cap are standbys — they still
serve direct traffic, just not this committee.

Runs after admission control and self-fill, so the cap applies to the final
worker set. Extracted as a helper to keep assemble_worker_pool under the
cognitive-complexity limit. 69 moa_gateway tests pass; clippy -D warnings and
fmt clean.
sim_all_workers_fail covers total failure (clean structured error). This covers
the common mesh reality: nodes flicker, so a subset of the committee dies
mid-turn while the rest answer. MoA must degrade to survivors, not the error
path.

Three cases pinned:
 - half the committee dies, rest answer -> turn completes, all 4 accounted
   (2 succeeded, 2 recorded failed, none silently dropped)
 - lone survivor (3 of 4 die) -> turn still answers
 - slow-failing worker -> does not stall the turn past the survivors

Locks the pre-existing best-effort fanout behaviour against regression now that
assembly (admission, self-fill, cap) is more complex. 214 moa tests pass;
clippy -D warnings and fmt clean.
workers.rs had grown to 1215 lines (over the 1k guideline) mixing two
responsibilities: worker-pool assembly/selection and the model backends +
config orchestration. Per CodeRabbit review, extract the assembly half.

pool.rs (658 lines) now owns: assemble_worker_pool, resolve_one_worker_from_
aliases, add_worker_backend, group_aliases_by_canonical_base, is_locally_served,
apply_admission_control, self_fill_from_extra_instances, cap_committee,
compute_actor_candidates, canonical_base_name, WorkerBackendResolution, the two
size caps, and their tests.

workers.rs (578 lines) keeps: build_moa_config (orchestrator), the
thinking-override helpers, patience_profile, the Local/Remote model backends +
parse_quic_http_response, and their tests.

Pure mechanical move, no behavior change. Backends and canonical_base_name made
pub(super) for cross-module use. 69 moa_gateway tests pass (unchanged); clippy
-D warnings and fmt clean.
`turn_completes_when_some_workers_die` asserted exactly succeeded==2/failed==2,
but early-exit consensus can abort a live worker once a usable answer is in
hand, so the succeeded/aborted split is timing-dependent. It passed locally but
raced red in CI (Rust crate tests shard 0).

The robustness contract is unchanged and still pinned: all four dispatched
workers appear in summaries (none silently dropped) and the two dead ones are
recorded as failed (>=2). Dropped the exact success-count assertion. Stable
across 5 consecutive local runs.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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 current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@evals/moa-openrouter/RESULTS.md`:
- Around line 164-180: Reconcile the trial-count inconsistency in the mesh-study
section by either changing “40 prompts × 3 draws” to match the reported n=80, or
explicitly documenting which 40 trials were excluded and the reason. Keep the
win/tie/loss totals and statistical results consistent with the final stated
sample size.
- Around line 23-38: Correct the significance summary in the results discussion
to acknowledge that the strong-actor Hermes-style cell is statistically
significant harm because its interval excludes zero. Update the statement that
no post-fix cell is significant, or explicitly document the adjusted statistical
test and its basis if that is intended to change the conclusion.
🪄 Autofix (Beta)

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: b6a3251c-8915-4646-b0b2-a9fbd22aa488

📥 Commits

Reviewing files that changed from the base of the PR and between 01d717d and 2309bca.

📒 Files selected for processing (40)
  • crates/mesh-llm-guardrails/src/rescue.rs
  • crates/mesh-llm-host-runtime/src/network/openai/ingress.rs
  • crates/mesh-llm-host-runtime/src/network/openai/moa_gateway/mod.rs
  • crates/mesh-llm-host-runtime/src/network/openai/moa_gateway/pool.rs
  • crates/mesh-llm-host-runtime/src/network/openai/moa_gateway/workers.rs
  • crates/mesh-mixture-of-agents/src/arbiter.rs
  • crates/mesh-mixture-of-agents/src/backend.rs
  • crates/mesh-mixture-of-agents/src/context.rs
  • crates/mesh-mixture-of-agents/src/fanout.rs
  • crates/mesh-mixture-of-agents/src/lib.rs
  • crates/mesh-mixture-of-agents/src/normalize.rs
  • crates/mesh-mixture-of-agents/src/reducer.rs
  • crates/mesh-mixture-of-agents/src/refinement.rs
  • crates/mesh-mixture-of-agents/src/tool_guard.rs
  • crates/mesh-mixture-of-agents/src/tool_turn.rs
  • crates/mesh-mixture-of-agents/src/worker.rs
  • crates/mesh-mixture-of-agents/tests/eval_openrouter.rs
  • crates/mesh-mixture-of-agents/tests/fixtures/ablation_tasks.json
  • crates/mesh-mixture-of-agents/tests/fixtures/committee_tasks.json
  • crates/mesh-mixture-of-agents/tests/fixtures/real_traces.json
  • crates/mesh-mixture-of-agents/tests/sim_all_workers_fail.rs
  • crates/mesh-mixture-of-agents/tests/sim_enable_thinking_propagation.rs
  • crates/mesh-mixture-of-agents/tests/sim_partial_worker_survival.rs
  • crates/mesh-mixture-of-agents/tests/sim_real_traces.rs
  • crates/mesh-mixture-of-agents/tests/sim_refinement_mesh_conditions.rs
  • crates/mesh-mixture-of-agents/tests/sim_strong_patience.rs
  • crates/mesh-mixture-of-agents/tests/sim_tool_call_text_not_passed_as_content.rs
  • crates/mesh-mixture-of-agents/tests/sim_tool_result_routes_to_reducer.rs
  • crates/mesh-mixture-of-agents/tests/sim_worker_accounting.rs
  • evals/moa-openrouter/README.md
  • evals/moa-openrouter/RESULTS.md
  • evals/moa-openrouter/agentic.jsonl
  • evals/moa-openrouter/analyze_ablation.py
  • evals/moa-openrouter/corpus.jsonl
  • evals/moa-openrouter/fanout.jsonl
  • evals/moa-openrouter/make_fixture.py
  • evals/moa-openrouter/orclient.py
  • evals/moa-openrouter/probe_tools.py
  • evals/moa-openrouter/record.py
  • evals/moa-openrouter/record_agentic.py
🚧 Files skipped from review as they are similar to previous changes (32)
  • crates/mesh-mixture-of-agents/tests/sim_worker_accounting.rs
  • crates/mesh-mixture-of-agents/tests/sim_all_workers_fail.rs
  • crates/mesh-llm-host-runtime/src/network/openai/ingress.rs
  • crates/mesh-llm-guardrails/src/rescue.rs
  • crates/mesh-mixture-of-agents/tests/sim_tool_call_text_not_passed_as_content.rs
  • crates/mesh-mixture-of-agents/tests/sim_strong_patience.rs
  • crates/mesh-mixture-of-agents/tests/sim_tool_result_routes_to_reducer.rs
  • crates/mesh-mixture-of-agents/tests/sim_enable_thinking_propagation.rs
  • crates/mesh-mixture-of-agents/src/normalize.rs
  • crates/mesh-mixture-of-agents/src/worker.rs
  • crates/mesh-mixture-of-agents/src/tool_guard.rs
  • crates/mesh-mixture-of-agents/src/backend.rs
  • crates/mesh-llm-host-runtime/src/network/openai/moa_gateway/workers.rs
  • crates/mesh-mixture-of-agents/tests/sim_partial_worker_survival.rs
  • crates/mesh-mixture-of-agents/tests/fixtures/real_traces.json
  • evals/moa-openrouter/agentic.jsonl
  • crates/mesh-mixture-of-agents/src/fanout.rs
  • crates/mesh-mixture-of-agents/tests/fixtures/committee_tasks.json
  • crates/mesh-mixture-of-agents/src/arbiter.rs
  • crates/mesh-llm-host-runtime/src/network/openai/moa_gateway/pool.rs
  • evals/moa-openrouter/corpus.jsonl
  • crates/mesh-mixture-of-agents/tests/eval_openrouter.rs
  • evals/moa-openrouter/fanout.jsonl
  • crates/mesh-mixture-of-agents/tests/fixtures/ablation_tasks.json
  • crates/mesh-llm-host-runtime/src/network/openai/moa_gateway/mod.rs
  • crates/mesh-mixture-of-agents/src/reducer.rs
  • crates/mesh-mixture-of-agents/src/refinement.rs
  • crates/mesh-mixture-of-agents/src/context.rs
  • crates/mesh-mixture-of-agents/src/lib.rs
  • crates/mesh-mixture-of-agents/src/tool_turn.rs
  • crates/mesh-mixture-of-agents/tests/sim_real_traces.rs
  • crates/mesh-mixture-of-agents/tests/sim_refinement_mesh_conditions.rs

Comment on lines +23 to +38
| strong (qwen3-32b) | original | 359/400 | −0.102 | [−0.170, −0.045] |
| strong (qwen3-32b) | Hermes-style | 385/400 | −0.037 | [−0.090, −0.003] |
| weak (qwen3-8b) | original | 365/400 | −0.013 | [−0.090, +0.070] |
| weak (qwen3-8b) | Hermes-style | **377/400** | **+0.017** | [−0.053, +0.100] |

Two monotonic effects:

1. **Fixing the packing helps in both actor conditions** (+0.065 strong,
+0.030 weak).
2. **References are worth more to a weaker actor** (+0.054 weak-vs-strong at
matched packing).

The only *statistically significant* cell in the matrix is the original-packing
strong-actor harm — i.e. the bug. After the fix, nothing is significant:
strong is marginal (upper bound −0.003), weak is a positive point estimate with
a CI spanning zero.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Correct the significance conclusion.

The strong/Hermes interval is [-0.090, -0.003]. It excludes zero. The text then says no post-fix cell is significant. State that this cell is significant harm, or document the adjusted test that changes this conclusion.

🤖 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 `@evals/moa-openrouter/RESULTS.md` around lines 23 - 38, Correct the
significance summary in the results discussion to acknowledge that the
strong-actor Hermes-style cell is statistically significant harm because its
interval excludes zero. Update the statement that no post-fix cell is
significant, or explicitly document the adjusted statistical test and its basis
if that is intended to change the conclusion.

Comment thread evals/moa-openrouter/RESULTS.md
# Conflicts:
#	crates/mesh-llm-guardrails/src/rescue.rs
#	crates/mesh-mixture-of-agents/src/normalize.rs
#	crates/mesh-mixture-of-agents/tests/sim_tool_call_text_not_passed_as_content.rs
`model=mesh` now works as a universal virtual model from one node upward:
  1 node / 1 model  -> serves that model (single-model passthrough)
  >=2 workers       -> real MoA committee (fan-out + synthesis)
  0 models          -> 503 (correct: nothing to serve)

Two bugs found and fixed against a live 2-node mesh (MacBook 3B + mini 1B):

1. request.raw not rewritten. degrade patched model_name/body_json but the
   forwarded request rides on request.raw bytes, which still said "mesh", so
   the embedded openai-frontend 404'd. Now uses rewrite_model_field (patches
   raw + body + Content-Length together; already unit-tested).

2. wrong model source. degrade read models_being_served(), empty on a fresh
   serve node. /v1/models draws from three sources; degrade now tries them in
   order: callable_models(targets) -> models_being_served() -> serving_models().

Live-verified: single node model=mesh returns HTTP 200 with a real answer
(was 503/404); 2-node mesh model=mesh returns x-moa-turn=fanout workers=2.

Known cold-start transient: the very first mesh request on a fresh serve node
can 404 for ~1-2s until the routing table populates; solid once warm.
@ndizazzo
ndizazzo self-requested a review August 5, 2026 02:39
Addresses two i386 P1s: destructive admission and committee-cap decisions were
made from a name heuristic that treats every unparseable name as big-tier, so a
small fine-tune/alias (`my-assistant`) could bypass the weak-worker filter or
become the reducer — contradicting the branch's core "weak can't soil strong"
claim.

Producer (each serving node, has its own GGUF): scan_gguf_total_parameters sums
tensor element counts for the exact stored-parameter count and publishes it via
the existing ServedModelMetadata.parameter_count_b. profile.rs prefers this over
name parsing; missing => None (unknown), never a guess.

Consumer (MoA orchestrator, has NO peer GGUFs): reads the gossiped
parameter_count_b off served descriptors. New SizeTier {Small,Big,Unknown}:
  * admission excludes only VERIFIED small, counts only VERIFIED big toward the
    ">=2 big remain" gate; Unknown is never strong and never filtered.
  * cap_committee ranks by verified size, NOT compute_actor_candidates
    (tool_use-first), which could evict a big model for tool-advertising small
    ones on an answer turn.
Name parsing (model_param_size_b) remains a lower-confidence fallback only.

71 moa_gateway tests (2 new: unknown-never-filtered, name-parse fallback);
model-artifact + host clippy -D warnings and fmt clean.
@michaelneale

Copy link
Copy Markdown
Collaborator Author

@i386 thanks - have it reading real data now

@michaelneale

Copy link
Copy Markdown
Collaborator Author

@i386 — fixed the tiering P1s you flagged (name-based tier / unparseable-name-as-big driving destructive admission). Now exactly your prescription: authoritative size from GGUF tensor sum, gossiped via the existing parameter_count_b, used for MoA tiering with name-parse only as a lower-confidence fallback and missing = Unknown (never big, never filtered).

Commit: 3b94cf0d (rebased/merged forward since). Three parts:

1. Producer computes the real sizecrates/model-artifact/src/gguf.rs, scan_gguf_total_parameters: sums every tensor's element count from the header/tensor-info table (no tensor data read):

pub fn scan_gguf_total_parameters(path: &Path) -> Option<u64> {
    let GgufHeader { file: mut f, n_tensors, n_kv } = open_gguf_header(path)?;
    skip_all_kv_pairs(&mut f, n_kv)?;
    let mut total: u64 = 0;
    for _ in 0..n_tensors {
        let _name = read_gguf_string(&mut f).ok()?;
        let n_dims = read_u32(&mut f).ok()?;
        if n_dims > MAX_GGUF_TENSOR_DIMS { return None; }
        let mut elements: u64 = 1;
        for _ in 0..n_dims {
            let dim = read_u64(&mut f).ok()?;
            elements = elements.checked_mul(dim)?;
        }
        let _ggml_type = read_u32(&mut f).ok()?;
        let _offset = read_u64(&mut f).ok()?;
        total = total.checked_add(elements)?;
    }
    Some(total)
}

2. Producer prefers it, falls back to namecrates/mesh-llm-host-runtime/src/models/profile.rs: parameter_count_b = tensor sum (÷1e9) when the local GGUF is present, else parameter_count_b_from_text(name), else None. Gossiped via ServedModelMetadata.parameter_count_b.

3. Consumer tiers off the gossiped valuecrates/mesh-llm-host-runtime/src/network/openai/moa_gateway/pool.rs. The MoA orchestrator has no peer GGUFs, so it reads gossiped parameter_count_b only:

enum SizeTier { Small, Big, Unknown }

fn tier_for(name: &str, sizes: &HashMap<String, f64>) -> SizeTier {
    if let Some(b) = sizes.get(&canonical_base_name(name)) {
        return if *b < SMALL_TIER_MAX_B { SizeTier::Small } else { SizeTier::Big };
    }
    match mesh_llm_guardrails::model_param_size_b(name) {   // name-parse fallback
        Some(b) if (b as f64) < SMALL_TIER_MAX_B => SizeTier::Small,
        Some(_) => SizeTier::Big,
        None => SizeTier::Unknown,                          // missing = Unknown, not big
    }
}

Admission excludes only verified Small and counts only verified Big toward the ">=2 big remain" gate — Unknown is never strong and never filtered. cap_committee also now ranks by this size, not the tool-actor ranking (your other P1 — a tool_use=None big model was getting evicted for tool-advertising small ones on answer turns).

Verified live on a 2-node mesh (MacBook 3B + mini Qwen3.5-4B): gossiped sizes went from a bogus name-parsed 4910 to real 3.21 / 4.21, and admission now no-ops on unknown-size workers. Two new unit tests: admission_never_filters_unknown_size_worker, admission_falls_back_to_name_parse_without_gossip.

Note deliberately deferred to follow-up: MoE active-parameter signal (total tensor count is the safety gate for now, as you said). Does this match what you had in mind?

@i386

i386 commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

Producer prefers it, falls back to namecrates/mesh-llm-host-runtime/src/models/profile.rs: parameter_count_b = tensor sum (÷1e9) when the local GGUF is present, else parameter_count_b_from_text(name), else None. Gossiped via ServedModelMetadata.parameter_count_b.

I think the fallback to name should be removed. if you cant get this info from gguf, the model must be ranked as the lowest param model.

As would be worth chucking anything in your cache at this and checking if that tensor counts matches the name to validate.

michaelneale and others added 2 commits August 5, 2026 14:24
Per i386: parsing NNb from a served name is brittle and a destructive
admission decision must not rest on an unverified label. Removed the name
fallback entirely.

Producer (profile.rs): parameter_count_b comes ONLY from the GGUF tensor sum.
No local GGUF to sum ⇒ None (no guessed count). Deleted the now-dead
parameter_count_b_from_text + its test.

Consumer (pool.rs): tier_for uses only the gossiped verified size. Dropped
SizeTier::Unknown — a model with no verified size is Small (weakest), so an
unverifiable label can never masquerade as big and displace a real strong
worker. cap_committee ranking updated (no-size ranks last). Admission excludes
only verified-small; with no verified big there is nothing to protect so an
all-unsized pool is untouched.

Tests updated: unsized-worker-is-weakest (excluded next to verified big),
no-verified-sizes keeps all. 71 moa_gateway + 28 profile tests pass; clippy -D
warnings and fmt clean.
@ndizazzo

ndizazzo commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

MeshLLM PR 1116 Homelab Validation Report

Field Value
Candidate commit ed90d75e156fbd6ea896f9a9edee5e427017deb6
Pull request Mesh-LLM/mesh-llm#1116, “MoA: ensembling that improves as mesh nodes join”
Base / current main PR base 70d4f3338c5020cdc9e05d1863a6b3787b5bed3c; tested current origin/main b7c26ebe789ec72a0a58db85d20b7c4a04e5832d
Previous/latest GitHub release v0.74.0, published 2026-07-27
Validation run ID 20260805T043845Z-ed90d75
Started / completed (UTC) 2026-08-05 04:38 / 2026-08-05 05:26
Decision NOT_READY for the claimed quality improvement; live MoA functionality PASS WITH DEFECTS

General Findings

PR 1116's MoA machinery works through the shipped OpenAI HTTP path on a real
three-node private homelab mesh. Fan-out, same-model self-fill, refinement,
reduction, streaming, single-actor tool calls, tool-result continuation, bounded
peer loss, and rejoin were all observed. All three final nodes report the same
mesh ID, two peers, a ready CUDA runtime, and no severe log events.

The PR is nevertheless NOT_READY for its headline claim that the shipped
mesh model improves as nodes join
. The committed results document explicitly
reports the shipped moa::handle_turn path at 9W/59T/12L (p = 0.66) versus the
best pool member and calls the unresolved harness-versus-production gap
merge-blocking. The stronger 49W/24T/6L, 48W/23T/2L, and 11W/68T/1L results
cannot be independently recomputed from committed trial outputs; the repo
contains corpora, small fixtures, scripts, and narrative results, but not the
scaled per-trial judge outputs. Re-running the paid OpenRouter studies and a
second judge was outside this homelab run.

Two additional release defects were found: repository just check-release
fails its Linux/aarch64 CUDA asset-parity assertion, and the chat endpoint accepts
missing or incorrectly typed messages and fabricates a 200 response. These do
not invalidate the live MoA observations, but they reinforce the NOT_READY
decision (unrelated).

Scope And Exclusions

Validated:

  • Fresh detached PR worktrees and native builds on mesh1, mesh2, and
    carrack.
  • CUDA 13.2 / SM87 on two Jetson AGX Orin 64 GB systems; CUDA 13.3 / SM120+SM86
    on RTX 5090 + RTX 3080.
  • Extracted product bundles, host import policy, private QUIC mesh, management
    status, /v1/models, chat completions, SSE, tool calls, tool results, negative
    requests, peer shutdown/rejoin, and final log scan.
  • Same-model topology using unsloth/Qwen3-0.6B-GGUF:Q4_K_M on all nodes.
  • Local Rust tests, formatting, and Clippy for MoA, guardrails, host runtime, and
    shipped binary.

Canonical Change Inventory And Validation Ledger

ID Category Release claim Planned checks Status Evidence / defects
MOA-1 FEATURE Answer turns draft, optionally refine, and aggregate Live non-stream and divergent answer request PASS 200 response; fanout, 4 successful proposal-stage calls (2 drafts + 2 refinements), reducer used once
MOA-2 FEATURE Same-model meshes self-fill from distinct nodes, capped at two active participants Three nodes, one identical model; inspect early-exit and layered accounting PASS Early-exit: 2/2 worker calls; divergent turn: 4/4 calls across draft+refine; reducer true
MOA-3 FEATURE Tool turns route to a single best actor Required tool request and continuation PASS Tool call had 1/1 worker, valid get_weather({city:"Toronto"}); continuation labeled tool-result
MOA-4 FEATURE Streaming remains OpenAI-compatible SSE request PASS 200 event stream, content chunk, terminal stop chunk, [DONE]
MOA-5 FEATURE Slow/absent peers do not stall turns Stop carrack, request through mesh1, rejoin carrack PASS Request returned in 949 ms with 2/2 successful calls; carrack rejoined same mesh
MOA-6 REVISION Weak worker admission control Unit/integration suite and source review PARTIAL Focused suite passes; not reproduced live with two distinct verified big models plus a weak model
MOA-7 BUG_FIX UTF-8 guardrail panic fixed Guardrails suite and final log scan PASS 15 guardrail tests pass; no panic/fatal/error in final logs
MOA-8 RESULT Shipped MoA improves over best member Review committed study and live functional evidence FAIL Own RESULTS.md says shipped path is parity: 9W/59T/12L, p=0.66, with unexplained orchestration gap
API-1 BUG_FIX Production OpenAI path remains contract-safe Valid, invalid JSON, missing/wrong messages, unknown model FAIL Invalid JSON 400 and unknown model 404; missing/wrong messages incorrectly return 200 with fabricated prose

Environment And Build Provenance

All worktrees were created at ~/dev/mesh/mesh-llm-pr1116, detached at the
exact PR SHA, then built with repository just recipes. dist/native-runtimes/
is untracked build output; source files remained unchanged.

Host alias OS / arch Hardware / backend Canonical build commands Product archive SHA-256 Manifest / import policy Status
mesh1.patio51.com Ubuntu 24.04 / aarch64 AGX Orin 64 GB, CUDA 13.2, SM87 just release-host-build; MESH_LLM_CUDA_TOOLKIT_MAJOR=13 LLAMA_STAGE_CUDA_ARCHITECTURES=87 just release-runtime-build cuda; just release-bundle-aarch64-cuda ... c7a80b3a66c32260533ed709f1a921928f14d94e0f3ea7cd43dc9ce564d033ca ABI 0.1.35, CUDA 13; zero rejected host imports PASS
mesh2.patio51.com Ubuntu 24.04 / aarch64 AGX Orin 64 GB, CUDA 13.2, SM87 same as mesh1 2f3941be95d91d9fcc10bdef7b7495e5618d95231baa4a67eff3372ff4973132 ABI 0.1.35, CUDA 13; zero rejected host imports PASS
carrack.patio51.com Arch Linux / x86_64 RTX 5090 + RTX 3080, CUDA 13.3, SM120+SM86 just release-host-build; MESH_LLM_CUDA_TOOLKIT_MAJOR=13 LLAMA_STAGE_CUDA_ARCHITECTURES='120;86' just release-runtime-build cuda; just release-bundle-cuda ... d1cc74704b88b5c837cd7c7dab481024efa4aec587ea5ee6fca3a084c6b9dd31 ABI 0.1.35, CUDA 13; zero rejected host imports PASS

patchelf 0.19.1 was installed on carrack because the release packaging script
requires it. The generic release-runtime-build cuda defaults its manifest to
CUDA 12 even when compiling with CUDA 13, so the explicit
MESH_LLM_CUDA_TOOLKIT_MAJOR=13 setting was required for runtime compatibility.

Product Validation Results

Area Case Expected Observed Status
Packaging Execute extracted products Host selects adjacent fresh CUDA 13 runtime All three selected ABI 0.1.35 runtime and loaded model PASS
Startup Fresh candidate startup API, console, model ready All hosts ready; final severe event count 0 PASS
Private mesh Three candidate nodes Same mesh ID, 2 peers each, private 380326a05b75a239f9f2ab2911d583b7, 2 peers each, private PASS
OpenAI API Same-model exact answer MoA observability and usable content early-exit, 2/2, 508 ms, MOA_OK. PASS
Inference Divergent reasoning answer Layered path and reducer fanout, 4/4 stage calls, reducer attempt 1, 2874 ms PASS
Streaming Chat SSE Valid chunks and [DONE] 3 data lines, expected content, [DONE] PASS
Tool calls Required call One actor and valid JSON arguments 1/1 worker, valid Toronto call PASS
Tool results Continue after result Final prose response tool-result, 1/1, 705 ms PASS
Reliability 3 loops each for auto and mesh Tool-call/result parity, streaming and non-streaming mesh 12/12; auto 10/12 PARTIAL
Nightly smoke chat, stream, tools on auto,mesh Stable success All configured functional steps passed; attestation unconfigured PASS
Negative API Invalid JSON / unknown model 4xx 400 / 404 PASS
Schema API Missing/wrong messages 400 200 with unrelated generated answer FAIL
Failure / recovery Remove and rejoin carrack Remaining mesh serves; same mesh restored 949 ms request succeeds; carrack rejoins with 2 peers PASS
Logs Severe event scan No panic/fatal/error mesh1 438 JSON lines, mesh2 316, carrack 284; 0 severe PASS
Release lane just check-release Clean aarch64 CUDA asset parity mismatch FAIL

Private Mesh Evidence

Final topology (left running):

Node API / console / QUIC Node ID Peers Model
mesh1 19337 / 13131 / 17842 00c6868b1b 2 unsloth/Qwen3-0.6B-GGUF:Q4_K_M
mesh2 19447 / 13145 / 17843 520112d875 2 same
carrack 19557 / 13155 / 17844 c45f63ec2e 2 same

API, Logs, UI, And Inference Findings

The same-model early-exit request produced 2 worker summaries, matching the
two-participant self-fill cap. The divergent request produced 4 summaries
because accounting includes both two draft calls and two refinement calls; it
does not imply four distinct nodes. Reduction ran once. Streaming deliberately
does not carry post-hoc x-moa-* headers, but emitted a valid OpenAI event
sequence.

The tool request selected one actor and returned structurally correct arguments.
Across the dedicated three-attempt reliability run, all 12 model=mesh phases
passed. Two model=auto tool-result phases omitted the fixture codeword; this is
consistent with weak-model output reliability and is not hidden by the aggregate.

No final log contained a structured error/fatal event or panic/fatal text.
Non-JSON lines were expected TUI/progress output. carrack used 3.8 GiB on its
RTX 5090 and 0.9 GiB on its RTX 3080 at the final snapshot. The Orin management
API reported roughly 59.3 GB usable VRAM per node.

Defects And Anomalies

Defect Severity Reproduction Expected / actual Release impact
Shipped quality path remains parity S1 See evals/moa-openrouter/RESULTS.md lines 427–492 Headline improvement / 9W-59T-12L, p=0.66 Blocks the claimed result; document itself calls it merge-blocking
Chat schema accepts absent/wrong messages S2 POST {"model":"mesh"} or string messages 400 / 200 fabricated unrelated prose Contract and safety defect
aarch64 release consistency failure S2 just check-release Pass / expected ...cuda.tar.gz, configured ...cuda-13.tar.gz Blocks clean release validation
Generic CUDA recipe stamps CUDA 12 by default S2 CUDA 13 host, just release-runtime-build cuda without override Detect installed toolkit / incompatible CUDA 12 manifest Fresh runtime rejected until explicit environment correction
model=auto tool-result omissions on 0.6B S3 Reliability harness, 3 attempts 12/12 / 10/12 Small-model route limitation; model=mesh was 12/12
Release build emits 28 unfulfilled lint-expectation warnings S3 just release-host-build on Orins Clean release output / warnings Build hygiene only; explicit Clippy gates pass

Risk And Follow-Up Register

Risk / gap Required action Status
Harness win does not survive shipped orchestration Diff exact prompt bytes and execution decisions, close parity gap, rerun the same study OPEN, blocking
Single-judge and absent raw trial-output risk Publish per-trial artifacts and rerun with a second judge OPEN, blocking for statistical claim
No live big-model quality comparison Run candidate through two verified 24–32B participants and a controlled baseline OPEN
Admission behavior only unit-tested here Add live two-big-plus-small topology assertion and record selected endpoint identities OPEN
OpenAI request schema too permissive Reject absent/non-array messages before MoA routing and add regression tests OPEN
Release metadata inconsistencies Fix CUDA asset parity and toolkit-major default/detection OPEN

Final Gate Assessment

Gate Result Evidence / rationale
All feature claims dispositioned PARTIAL Runtime behavior covered; statistical lift and live admission quality not proven
Canonical packages proven on real hosts PASS Three extracted CUDA 13 products run on two architectures
APIs, logs, and inference proven PARTIAL Core paths pass; request schema defect remains; no visual UI check
Candidate private mesh proven PASS Three nodes, same mesh ID, two peers each, failure/rejoin tested
Required mixed-version compatibility NOT_APPLICABLE Homogeneous PR request; no claimed protocol/ABI change
No open S0/S1 defect FAIL Headline shipped-path quality claim remains unsupported/parity
Required CI lanes terminal PASS Required GitHub checks passed; irrelevant matrix lanes skipped
Residual risk accepted and bounded FAIL Core improvement claim requires new evidence, not a waiver

Sign-Off

Decision: NOT_READY for merge/release on the stated quality-improvement
claim.
The live three-node MoA feature is functional and can remain running for
further investigation. Exact next actions: fix the shipped-path parity gap and
API schema defect, make the release consistency gate pass, publish/recompute
per-trial evidence with a second judge, then repeat a live capable-model mesh
comparison.

Self-fill could re-add the same physical node (or an endpoint already backing
the sole worker), turning ONE box into a fake 2-worker committee that hits it
twice for near-identical drafts. Catastrophic for mesh mode: a lone node must
degrade to single-model serving, never pretend to be a committee.

Rewrote self_fill_from_extra_instances to build the pool from DISTINCT physical
endpoints only: the local skippy port (if this node serves the model and
context fits) plus each distinct remote peer from hosts_for_model. If fewer
than two distinct endpoints serve the model, the pool stays the single worker
and MoA degrades to single-model. A single endpoint can no longer appear twice.

Also fixes the CodeRabbit self-fill bugs (re-adds same peer; skipped context
eligibility) — context fit is now checked on the local endpoint, and endpoints
are distinct by construction.

71 moa_gateway tests pass; clippy -D warnings and fmt clean.
A `model=mesh` request with no `messages`, a non-array `messages`, or an empty
array fell through to the workers and fabricated a 200 answer from nothing
(homelab validation API-1). try_handle_moa now requires a present, non-empty
`messages` array and returns 400 otherwise, before any model call.

Verified live: {"model":"mesh"}, string messages, and [] all now 400; a valid
request still returns 200. 71 moa_gateway tests pass; clippy -D warnings + fmt
clean.
@michaelneale

Copy link
Copy Markdown
Collaborator Author

@ndizazzo the cuda thing seems unrelated, and can you try that again with non trivial model as i can't see that model yielding much at all, so probably results are too noisy.

@michaelneale

michaelneale commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator Author

@ndizazzo yeah the claim is only for larger models combining (20B ish) unfortunately, so need to test with that (still probing myself). Also want to make sure this improves over the main branch.

but this reminds me to try it with larger combos... so standby.. (in the mean time feel free to try it with larger - and some subsequent fixes)

Width sprint (evals/moa-openrouter, aggregator qwen3-8b, 8B peers, shipped
committee path) shows the fan-out cap was throttling the pools that benefit
most:

  6× diverse 8B vs best member: 12W/65T/2L, p=0.013  (wins)
  4× diverse 8B:                 5W/73T/0L, p=0.06   (marginal)
  2× diverse 8B:                 2W/77T/1L, p=1.0    (null)

Small, weak drafts need WIDTH — more independent proposals — before
aggregation clears the best member. A verified big model, by contrast, wins at
2 and gains nothing past ~4.

- committee_cap is now tier-aware: COMMITTEE_CAP_SMALL=6 for all-small pools,
  COMMITTEE_CAP_BIG=4 when a verified big model is present (replaces the flat
  MAX_COMMITTEE_WORKERS=4).
- eval harness: the three per-trial judge comparisons now run concurrently
  (tokio::join!) instead of three serial awaits — ~3x faster judging phase, no
  behaviour change.

Also measured (recorded in RESULTS.md): the refine round never beats single
aggregation at 8B (refine-vs-single null in every cell) — Hermes' cheaper
single-synth cadence matches Together's layered shape here; and at 8B diversity
matters (6 diverse >> 6 same), unlike mid-scale.

72 moa_gateway tests (new: committee_cap_is_wide_for_small_pools_tight_for_big);
clippy -D warnings and fmt clean.
@michaelneale

Copy link
Copy Markdown
Collaborator Author

Update: width sprint + review fixes (since ed90d75e)

Addressed the homelab report and pushed several fixes. Key finding first.

Small-model meshes DO improve — with width, not depth

The homelab run used Qwen3-0.6B, an order of magnitude below the smallest model in our data (8B). Our committed table shows an 8B floor (2–3× 8B is null; 4× wins), so a 0.6B null is the expected result, not a design signal.

A fresh sprint (aggregator qwen3-8b, 8B-class peers, shipped committee path, position-swapped judge, length-noted) found the real lever is committee width:

pool single-agg vs best member refine vs single-agg
2× 8B diverse 2W/77T/1L, p=1.0 null
4× 8B diverse 5W/73T/0L, p=0.06 null
6× 8B diverse 12W/65T/2L, p=0.013 null
6× 8B same 4W/75T/1L, p=0.38 null

Three results:

  1. Six diverse 8B models beat the best single member (p=0.013). The old flat MAX_COMMITTEE_WORKERS=4 throttled exactly the small pools that need width. Cap is now tier-aware: 6 for all-small, 4 when a verified big model is present (a 24–32B pair already wins at 2).
  2. The refine round never earns its serial costrefine vs single-agg is null in every cell. Hermes' single-aggregation cadence matches Together's layered shape here at half the latency. (Noted for follow-up; not changed in this PR.)
  3. At 8B, diversity matters (6 diverse 12W/2L ≫ 6 same 4W/1L) — unlike mid-scale where Self ≈ Mixed.

Review fixes pushed

  • i386 P1 ×2: tiering now uses verified GGUF tensor-sum size gossiped via parameter_count_b (not name parsing); cap_committee ranks by size, not tool_use. Name fallback removed per follow-up — no verified size ⇒ ranked weakest. Validated: cached 3B→3.21, 4B→4.21; gemma-4-E4B name says 4B but sums to 7.5B (proves the name heuristic was unsafe).
  • Iron law: self-fill rebuilt to use distinct physical endpoints only — a single node can never fake a 2-worker committee; it degrades to single-model. (Also closes the two CodeRabbit self-fill bugs.)
  • API-1: model=mesh with missing/non-array/empty messages now returns 400, not a fabricated 200. Verified live.
  • CodeRabbit: pool.rs extracted; eval judges parallelised.

Still open (agreed)

  • Capable-model (24–32B) shipped-path run + second judge for the headline quality claim — needs paid runs, separate effort.
  • aarch64 check-release parity + CUDA-toolkit-default: pre-existing, not touched by this branch (check-release passes on this HEAD; branch changes no release/packaging code).

Full data + method in evals/moa-openrouter/RESULTS.md.

…is dead cost

The width sprint measured refine-vs-single-aggregation as null in every 8B cell
(2/4/6 workers, diverse and same). Small pools win by WIDTH under a single
aggregation, not by the extra serial refine pass. Previously RefinementPolicy::
Auto ran the round for ALL-small pools — paying two synthesis passes for no
measured gain.

Auto now refines only for a homogeneous pool that is NOT all-small (i.e.
same-model at real scale, where correlated drafts + the round measured 48/2 vs
35/10). All-small and diverse pools skip it — matching Hermes' cheaper
single-synth cadence, where refine buys nothing.

Effect: an all-small mesh turn drops from 2 serial synthesis passes to 1,
roughly halving added latency, with no measured quality loss.

Tests: auto_skips_an_all_small_pool (was auto_refines_...); the 5 all-small
mechanics sim tests (straggler/grace/degradation) pinned with Always so they
still exercise the round; big-pool gate tests keep Auto. 178 + sim tests pass;
clippy -D warnings and fmt clean.
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.

3 participants