Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
474f400
feat(providers): add an idle-only timeout mode to ApiClient for local…
Broccolito Oct 10, 2026
33ade7e
fix(local): preserve prompts and safely execute local model tool calls
Broccolito Oct 11, 2026
fa4f76f
feat(server): expose local model recommendations and context windows
Broccolito Oct 11, 2026
ebd4baf
fix(cli): align local model selection and cache management
Broccolito Oct 11, 2026
56366c0
fix(desktop): show local provider suitability and live context
Broccolito Oct 11, 2026
8be80b4
docs: record local inference behavior and validation
Broccolito Oct 11, 2026
dfd0db5
test(providers): enlarge the model survey bootstrap fixture
Broccolito Oct 12, 2026
98fd881
docs(providers): record completed local inference validation
Broccolito Oct 12, 2026
f28ca99
fix(agent): end an unrecoverable context overflow as a stored verdict…
Broccolito Oct 12, 2026
5b879a6
fix(ollama): refuse tools to a model whose template never shows them …
Broccolito Oct 12, 2026
359f032
fix(code-execution): say which calls completed and which read failed …
Broccolito Oct 12, 2026
49f519e
docs(providers): correct the Harmony claim in text-call recovery (F06)
Broccolito Oct 12, 2026
b672d90
fix(ollama): ask once when a whole reply imitates a tool result (F04,…
Broccolito Oct 12, 2026
d281bd6
fix(code-execution): point console, return and silent scripts at reco…
Broccolito Oct 12, 2026
4010c2f
fix(code-execution): keep a tool's large-integer output exact (F13)
Broccolito Oct 12, 2026
0e38ef2
fix(code-execution): give a script a failed call's output, not only i…
Broccolito Oct 12, 2026
21fd664
fix(code-execution): name the import when a script calls an unimporte…
Broccolito Oct 12, 2026
35954e3
fix(code-execution): say a silent script's calls succeeded and need n…
Broccolito Oct 12, 2026
46d2042
docs(providers): record the follow-up on the 14 failed live attempts
Broccolito Oct 12, 2026
acbefe5
fix(code-execution): explain a recorded property of a string result (…
Broccolito Oct 12, 2026
6740288
docs(providers): record the live rerun of the failed attempts and for…
Broccolito Oct 12, 2026
8f90576
fix(code-execution): hand scalar tool output to scripts as text, and …
Broccolito Oct 12, 2026
8e18644
fix(agent): title a chat without calling a provider that just failed …
Broccolito Oct 12, 2026
65be50c
fix(todo): read loose checklist spacing, and refuse a write that name…
Broccolito Oct 12, 2026
b24542d
docs(agent): say what the title call did after an overflowed turn (F1…
Broccolito Oct 12, 2026
5a2f396
docs(providers): record the review of the rerun, its fixes and the af…
Broccolito Oct 12, 2026
f20c73d
docs(code-execution): say which tool results a script receives parsed
Broccolito Oct 12, 2026
6cd995b
docs(code-execution): say when a script's result names its undefined …
Broccolito Oct 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
398 changes: 347 additions & 51 deletions CLAUDE.md

Large diffs are not rendered by default.

24 changes: 23 additions & 1 deletion biorouter-self-test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ activities:
- Test error boundaries and security controls
- Validate the Settings reset contract and its active-work safety gate
- Verify provider error handling, conversation-scoped reasoning, and saved-credential deletion safeguards
- Verify local inference executes tools and preserves results across turns
- Validate Crew empty-state onboarding, isolated Alice/Bob/Carol development profiles, and private-by-default workspace presentation
- Exercise Crew first-host preparation and SSH trust validation with synthetic invitation metadata only
- Verify Crew request idempotency, human-only session access, and scoped normal-chat tool denial paths
Expand All @@ -36,7 +37,7 @@ parameters:
input_type: string
requirement: optional
default: "all"
description: "Which test phases to run: all, basic, extensions, computer_use, crew, apps, previews, subagents, knowledge, workflows, reset, providers, office, advanced"
description: "Which test phases to run: all, basic, extensions, computer_use, crew, apps, previews, subagents, knowledge, workflows, reset, providers, local_inference, office, advanced"

- key: test_depth
input_type: string
Expand Down Expand Up @@ -893,6 +894,27 @@ prompt: |
Log results to: {{ workspace_dir }}/phase4c_provider_boundaries.md
{% endif %}

{% if test_depth != "fixture_only" and (test_phases == "all" or "local_inference" in test_phases) %}
## PHASE 4D: Local Inference Tool Execution

Run this phase only when the harness has selected an approved local Ollama or Llama Server
model. Otherwise report BLOCKED. Keep the selected provider, model and settings unchanged.
Work only in {{ workspace_dir }}/local-inference/. Do not build the repository, download
models, inspect credentials, or change the user's Ollama daemon settings.

1. Use a real tool to create factorial.py in that directory. It must print math.factorial(10).
Execute the saved file, require exit 0 and stdout exactly 3628800, then read it back.
2. Create notes.txt containing exactly these lines: urine, plasma, URINE, serum, urine,
saliva, Urine, tissue, urine. Count case-insensitive whole-word matches using a tool.
Require exactly 5 and identify the saved file in the answer.
3. Read notes.txt again in a later tool call and independently verify the same count.
A description of a command, invented tool name, or JSON displayed instead of a real
tool result is FAIL. Record the tool names and actual results.
4. Write phase4d_local_inference.md with PASS or FAIL for each assertion and any observed
provider error. Do not claim a context window or memory measurement that was not
exposed by the harness. The external verifier checks the saved scripts and outputs.
{% endif %}

## 📊 PHASE 5: Final Report Generation

{% if test_depth == "fixture_only" %}
Expand Down
70 changes: 68 additions & 2 deletions crates/biorouter-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1119,14 +1119,18 @@ enum LocalModelCommand {
/// Download a local model into the shared cache without starting a chat
#[command(about = "Download (pre-cache) a local model")]
Pull {
#[arg(help = "Catalog model name (e.g. qwen3.5-4b) or a raw owner/repo:QUANT HF spec")]
#[arg(
help = "Catalog model name (qwen3.8-27b or gemma4-31b) or a raw owner/repo:QUANT HF spec; a retired catalog name downloads the model a chat naming it runs"
)]
model: String,
},

/// Delete a downloaded local model to reclaim disk space
#[command(about = "Delete a downloaded local model", visible_alias = "delete")]
Rm {
#[arg(help = "Catalog model name (e.g. gemma4) or a raw owner/repo:QUANT HF spec")]
#[arg(
help = "Catalog model name (e.g. qwen3.8-27b), a retired catalog name (e.g. gemma4) to free the files it left, or a raw owner/repo:QUANT HF spec"
)]
model: String,
},
}
Expand Down Expand Up @@ -3857,3 +3861,65 @@ mod default_max_turns_tests {
);
}
}

#[cfg(test)]
mod local_model_help_tests {
use super::Cli;
use biorouter::providers::llamacpp::{MODEL_CATALOG, RETIRED_MODELS};
use clap::CommandFactory;

fn model_arg_help(subcommand: &str) -> String {
let mut command = Cli::command();
let local = command
.find_subcommand_mut("models")
.and_then(|models| models.find_subcommand_mut("local"))
.expect("`biorouter models local` exists");
let sub = local
.find_subcommand_mut(subcommand)
.unwrap_or_else(|| panic!("`biorouter models local {subcommand}` exists"));
let help = sub
.get_arguments()
.find(|arg| arg.get_id() == "model")
.and_then(|arg| arg.get_help())
.map(|help| help.to_string());
help.unwrap_or_else(|| panic!("`{subcommand}` has a model argument with help"))
}

/// `pull`'s help named `qwen3.5-4b` through two catalog changes: an example
/// that, followed, downloaded a model no chat would run. It must name only
/// models the catalog offers, and the clap `help` string cannot read the
/// catalog itself, so this keeps the literal honest.
#[test]
fn pull_help_names_the_catalog_and_no_retired_model() {
let help = model_arg_help("pull");
for entry in MODEL_CATALOG {
assert!(help.contains(entry.name), "{help}");
}
for retired in RETIRED_MODELS {
let named = help
.split(|c: char| c.is_whitespace() || "(),;".contains(c))
.any(|word| word == retired.name);
assert!(!named, "pull help names retired {}: {help}", retired.name);
}
}

/// `rm` may name a retired model (deleting its files is what it is for), but
/// its first example must be a model the catalog offers.
#[test]
fn rm_help_leads_with_a_catalog_model() {
let help = model_arg_help("rm");
let first_catalog_name = MODEL_CATALOG
.iter()
.filter_map(|entry| help.find(entry.name))
.min()
.expect("rm help names a catalog model");
let first_retired_name = RETIRED_MODELS
.iter()
.filter_map(|retired| help.find(&format!("e.g. {}", retired.name)))
.min();
assert!(
first_retired_name.is_none_or(|at| first_catalog_name < at),
"{help}"
);
}
}
Loading
Loading