Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
de77504
Added a NUnit test project for the whole solution
SommerEngineering Sep 11, 2026
87566c7
Add a model corpus and characterization tests for the capability rules
SommerEngineering Sep 11, 2026
3302915
Add the model matching engine and make capabilities a flags enum
SommerEngineering Sep 11, 2026
d3b2813
Add the model family API, its compile-time registry, and MWAIS0013
SommerEngineering Sep 11, 2026
31b655c
Add one model host per provider and the unwrapping walk
SommerEngineering Sep 11, 2026
730bd1c
Port the OpenAI families and add the model registry
SommerEngineering Sep 11, 2026
c6cbce6
Port the Anthropic and Google families
SommerEngineering Sep 11, 2026
4308670
Spelling
SommerEngineering Sep 11, 2026
31a1aec
Port the Mistral families, including the release date logic
SommerEngineering Sep 11, 2026
5905f07
Port the Alibaba Cloud families and refuse ambiguous rule inheritance
SommerEngineering Sep 11, 2026
402da2d
Port the DeepSeek, Perplexity, and xAI families
SommerEngineering Sep 11, 2026
9c65ab6
Port the Llama and Qwen open weights
SommerEngineering Sep 11, 2026
ad3a25c
Port the Mistral open weights and the GLM family
SommerEngineering Sep 11, 2026
ac547cc
Port the Gemma, Nemotron, and Phi families
SommerEngineering Sep 12, 2026
7eec39c
Port the remaining open weights we decided to name
SommerEngineering Sep 12, 2026
77c95e6
Fixed warnings
SommerEngineering Sep 12, 2026
9307f2c
Move the model kinds into the rule engine
SommerEngineering Sep 12, 2026
429ca8c
Resolve model profiles through the registry
SommerEngineering Sep 12, 2026
77130a4
Read model capabilities from the profile everywhere
SommerEngineering Sep 12, 2026
b6defc8
Keep non-chat models out of the chat lists
SommerEngineering Sep 12, 2026
51613e3
Delete the old capability rules
SommerEngineering Sep 12, 2026
849f072
Answer the model kind from the registry
SommerEngineering Sep 12, 2026
f407885
Split the reasoning parameters into dialects
SommerEngineering Sep 12, 2026
6d80e6d
Let organizations describe their own models
SommerEngineering Sep 12, 2026
e8ec2c1
State the context window where a vendor documents one
SommerEngineering Sep 12, 2026
5ee4685
Count the whole conversation against the model's window
SommerEngineering Sep 12, 2026
8468cdc
Recompute the token count instead of announcing it
SommerEngineering Sep 12, 2026
4d43164
Check image counts against the limits vendors state
SommerEngineering Sep 12, 2026
2509fc0
Say when a chat carries more images than the model takes
SommerEngineering Sep 12, 2026
5326a0b
Name the tokenizer a model uses, where its vendor does
SommerEngineering Sep 12, 2026
672237a
Tell embedding providers which tokenizer their model uses
SommerEngineering Sep 12, 2026
8fdef4a
Let the snapshot cover every model of the corpus
SommerEngineering Sep 12, 2026
bb22b3c
Let a provider state its own window and image limits
SommerEngineering Sep 12, 2026
d8a30c4
Add the model limits to the expert settings
SommerEngineering Sep 13, 2026
1c03a46
Read the context window from a provider's model list
SommerEngineering Sep 13, 2026
caf2aba
Read the context window from four more model lists
SommerEngineering Sep 13, 2026
58b9aa4
Add a quality gate for tests, Clippy, and the model sources
SommerEngineering Sep 13, 2026
5616015
Document the model capability system
SommerEngineering Sep 13, 2026
ed17f70
Updated changelog
SommerEngineering Sep 13, 2026
08b6a19
Fix the verify job on a fresh checkout
SommerEngineering Sep 13, 2026
9c3fcbf
Fix the Clippy findings in the Linux-only code paths
SommerEngineering Sep 13, 2026
142f480
Name the model's window when a text was too long
SommerEngineering Sep 13, 2026
84679be
Remove the capability repair the profile made impossible
SommerEngineering Sep 13, 2026
3ace546
Updated I18N
SommerEngineering Sep 13, 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
87 changes: 86 additions & 1 deletion .github/workflows/build-and-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -724,9 +724,94 @@ jobs:
overwrite: true
retention-days: ${{ env.RETENTION_INTERMEDIATE_ASSETS }}

#
# The quality gate. Deliberately without an `if` on build_enabled: a pull request only builds
# when somebody sets the run-pipeline label, and a gate which is closed exactly while nobody is
# looking is not a gate. It is cheap for the same reason it is unconditional -- one platform, no
# Tauri bundle, no signing, no artifacts.
#
verify:
name: Verify
runs-on: ubuntu-latest
permissions:
contents: read

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Read the toolchain versions from the metadata
run: |
# The .NET SDK version. The format is '9.0.205 (commit 3e1383b780)',
# so we extract the version number alone:
dotnet_sdk_version=$(sed -n '4p' metadata.txt | sed 's/[^0-9.]*\([0-9.]*\).*/\1/')

# The Rust version, written the same way:
rust_version=$(sed -n '6p' metadata.txt | sed 's/[^0-9.]*\([0-9.]*\).*/\1/')

echo "DOTNET_SDK_VERSION=${dotnet_sdk_version}" >> $GITHUB_ENV
echo "RUST_VERSION=${rust_version}" >> $GITHUB_ENV

echo ".NET SDK version: '${dotnet_sdk_version}'"
echo "Rust version: '${rust_version}'"

- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: ${{ env.DOTNET_SDK_VERSION }}
cache: true
cache-dependency-path: 'app/MindWork AI Studio/packages.lock.json'

- name: Cache Rust
uses: actions/cache@v4
with:
path: |
~/.cargo/git/db/
~/.cargo/registry/index/
~/.cargo/registry/cache/
runtime/target

key: verify-linux-x64-rust-${{ env.RUST_VERSION }}

- name: Setup Rust (stable)
uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ env.RUST_VERSION }}
components: clippy

- name: Setup dependencies (Ubuntu-specific)
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf libfuse2 xdg-utils gstreamer1.0-plugins-base gstreamer1.0-plugins-good

- name: Place stand-ins for what Tauri's build script expects
run: |
# Tauri's build script insists that everything the configuration lists is already there,
# and refuses to run otherwise -- so nothing Rust compiles without it. Two of those
# things are products of a build which has not run here: the .NET app as a sidecar, and
# the PDF library which the build downloads into the resources. The other two resource
# directories, notices and tokenizers, are in the repository and need nothing.
#
# This job never bundles anything and never starts the app; it compiles, tests and lints
# the Rust code, and no test opens either file. Empty stand-ins are therefore enough,
# while publishing the sidecar and downloading the library would cost minutes for files
# nobody here reads. Should a Rust test ever need the real library, this job has to
# deploy it the way build_main does instead of placing a stand-in.
mkdir -p "app/MindWork AI Studio/bin/dist"
touch "app/MindWork AI Studio/bin/dist/mindworkAIStudioServer-x86_64-unknown-linux-gnu"
chmod +x "app/MindWork AI Studio/bin/dist/mindworkAIStudioServer-x86_64-unknown-linux-gnu"

mkdir -p runtime/resources/libraries
touch runtime/resources/libraries/stand-in-for-verify.txt

- name: Run the quality gate
run: |
cd "app/Build"
dotnet run verify

build_main:
name: Build app (${{ matrix.dotnet_runtime }})
needs: [determine_run_mode, read_metadata]
needs: [determine_run_mode, read_metadata, verify]
if: needs.determine_run_mode.outputs.build_enabled == 'true'
permissions:
contents: read
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -172,3 +172,6 @@ orleans.codegen.cs

# Tauri generated schemas/manifests
/runtime/gen/

# Ignore what a failing snapshot test leaves behind for comparison:
/app/Tests/Models/Corpus/CapabilitySnapshot.actual.txt
34 changes: 32 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,21 @@ Notes:
troubleshooting, no matter whether it came from the MCP server or from the user.

### Running Tests
Currently, no automated test suite exists in the repository.
The .NET tests live in `app/Tests`, a single NUnit project that holds the tests of every area; each
area gets its own folder and namespace below it rather than a project of its own. Agents run them
through the IDE for the same reason they build there:

```
mcp__rider__execute_terminal_command command: "cd app/Tests && dotnet test"
```

An assembly-wide `[SetUpFixture]` in `app/Tests/TestHost.cs` fills the static application state that
the app itself only fills while starting up, `Program.LOGGER_FACTORY` above all. Types that
initialize a static logger from it — `Settings.Provider` among them — otherwise die in their type
initializer before the first assertion. Prefer writing new code so that it does not reach for such
statics at all.

The Rust tests run with `cargo test` in `runtime/`, through the `rustrover` MCP server.

## Architecture Details

Expand Down Expand Up @@ -141,7 +155,8 @@ Key structure:
Plugins are written in Lua and provide:
- **Language plugins** - I18N translations (e.g., German language pack)
- **Configuration plugins** - Enterprise IT configurations for centrally managed providers, settings
- **Future:** Assistant plugins for custom assistants
- **Assistant plugins** - custom assistants and direct-chat launchers, subject to approval or a local security audit
- **Model plugins** - what an organization's own models can do, see `documentation/Models.md`

**Example configuration plugin:** `app/MindWork AI Studio/Plugins/configuration/plugin.lua`

Expand Down Expand Up @@ -177,6 +192,21 @@ When adding, changing, or removing model-driven tools, keep these parts in sync:

Tool implementations must treat model-provided arguments as untrusted input. Validate settings and arguments, protect secrets with `SensitiveTraceArgumentNames`, use `ToolExecutionBlockedException` for intentional policy blocks, and check provider confidence before returning sensitive data to the model.

## Model Capabilities

**Documentation:** `documentation/Models.md`

What a model can do is answered in `app/MindWork AI Studio/Models/`, through `provider.GetModelProfile()`. Never ask `ModelRegistry` directly from a component: the extension method is what adds the expert settings and what a provider's model list reported, and the registry alone answers neither.

When adding, changing, or removing model knowledge, keep these parts in sync:
- `app/MindWork AI Studio/Models/<Vendor>/<Family>.cs` for the family itself. Creating the class is enough — the source generator in `app/SourceGeneratedMappings/` collects every non-abstract `ModelFamily` and `IModelHost` at compile time, so there is no registration list. Do not add reflection here; `PublishTrimmed` is on.
- `app/Tests/Models/Corpus/` for the model IDs the family covers, marked as either unchanged or expected to change. A porting difference which nobody declared is what the corpus exists to catch.
- `app/MindWork AI Studio/Models/Kinds/` when the change is about what kind of model something is, rather than what it can do. These are ordinary rules of the same engine.
- `app/MindWork AI Studio/Models/Hosting/Hosts/` when a provider wraps model names or cannot pass an API through. A host unwraps and trims the transport; it states nothing about the model itself.
- `app/MindWork AI Studio/Plugins/models/plugin.lua` when a new field can be declared by an organization, and `app/MindWork AI Studio/Plugins/configuration/plugin.lua` when it can be overridden per provider instance.

Rules are never tried in order: specificity is computed from the rule, and two rules of equal specificity on one name fail the test suite. State how a model reasons with `Reasoning(...)` — the three reasoning capabilities are override vocabulary and must never appear in a profile. Every family and every host has to name the page it was read from and the day somebody read it; `dotnet run verify-models` reports the ones which have gone stale.

## RAG (Retrieval-Augmented Generation)

RAG integration is currently in development (preview feature). Architecture:
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,8 @@ You want to know how to build MindWork AI Studio from source? [Check out the ins

Do you want to add or maintain model-driven tools? [Read the tool development guide here](documentation/Tools.md).

Do you want to teach AI Studio what a model can do? [Read the model capabilities guide here](documentation/Models.md).

</details>

<details>
Expand Down
22 changes: 17 additions & 5 deletions app/Build/Commands/UpdateMetadataCommands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,10 @@ private async Task BuildPreparedRelease(bool offline)
await new CollectI18NKeysCommand().CollectI18NKeys();

// Build the final release, where Rust knows the updated metadata, the .NET
// artifacts are already in place, and .NET knows the updated web assets, etc.:
await this.Build(offline);
// artifacts are already in place, and .NET knows the updated web assets, etc.
// The gate already ran in the first build; running it a second time on the same
// sources would only add minutes:
await this.Build(offline, skipVerify: true);
}

[Command("update-metainfo", Description = "Update the AppStream metainfo entry of one release from its changelog")]
Expand Down Expand Up @@ -221,11 +223,21 @@ private async Task UpdateReleaseDependenciesAndLicence()

[Command("build", Description = "Build MindWork AI Studio")]
public async Task Build(
[Option("offline", Description = "Skip downloads and use locally available build dependencies")] bool offline = false)
[Option("offline", Description = "Skip downloads and use locally available build dependencies")] bool offline = false,
[Option("skip-verify", Description = "Skip the quality gate which otherwise runs before anything is built")] bool skipVerify = false)
{
if(!Environment.IsWorkingDirectoryValid())
return;


//
// The gate runs before anything is built, and the build stops when it does not pass. That
// way the same command answers both questions a person has -- is it sound, and does it
// build -- and answers them in that order, because building something the tests reject
// takes minutes to produce an artifact nobody should use.
//
if (!skipVerify && await new VerifyCommand().Verify() is not 0)
throw new CommandExitedException(1);

//
// Build the .NET project:
//
Expand Down Expand Up @@ -596,7 +608,7 @@ private static async Task WriteMetainfoRelease(string appVersion, DateTime relea

// Drop any earlier entry of this version, so that the version stays unique and moves to the top.
// We remove from the back, so that the index of the remaining matches stays valid:
foreach (var previousRelease in ReleaseBlockRegex().Matches(metainfo).Cast<Match>().Where(match => ReleaseTagHasVersion(match.Value, appVersion)).Reverse())
foreach (var previousRelease in ReleaseBlockRegex().Matches(metainfo).Where(match => ReleaseTagHasVersion(match.Value, appVersion)).Reverse())
metainfo = metainfo.Remove(previousRelease.Index, previousRelease.Length);

var lineEnding = metainfo.Contains("\r\n", StringComparison.Ordinal) ? "\r\n" : "\n";
Expand Down
92 changes: 92 additions & 0 deletions app/Build/Commands/VerifyCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
using Build.Tools;

// ReSharper disable ClassNeverInstantiated.Global
// ReSharper disable UnusedType.Global
// ReSharper disable UnusedMember.Global
namespace Build.Commands;

/// <summary>
/// The quality gate: one command, the same one locally and in the pipeline.
/// </summary>
/// <remarks>
/// Every check runs, even after one of them has failed. A gate which stops at the first failure
/// tells you one thing per run, and the next run costs the same minutes again -- while the point of
/// running the whole thing is to learn everything which is wrong in one go.
/// </remarks>
public sealed class VerifyCommand
{
/// <summary>
/// How the .NET app is named once it lies where Tauri expects it.
/// </summary>
private const string SIDECAR_PREFIX = "mindworkAIStudioServer-";

[Command("verify", Description = "Run the quality gate: .NET tests, Rust tests, Clippy, and the model sources")]
public async Task<int> Verify()
{
if(!Environment.IsWorkingDirectoryValid())
return 1;

Console.WriteLine("==============================");
Console.WriteLine("- Quality gate: every check runs, so that the first failure does not hide the next ...");

var results = new List<(string What, int ExitCode)>
{
(".NET tests", await CommandRunner.RunAsync(Environment.GetTestsDirectory(), "dotnet", "test --nologo")),
};

var runtimeDirectory = Environment.GetRustRuntimeDirectory();
if (WhatTauriExpectsIsThere())
{
results.Add(("Rust tests", await CommandRunner.RunAsync(runtimeDirectory, "cargo", "test")));
results.Add(("Clippy", await CommandRunner.RunAsync(runtimeDirectory, "cargo", "clippy --all-targets -- -D warnings")));
}
else
{
//
// Tauri's build script insists that everything the configuration lists is already
// there and refuses to run otherwise, so nothing Rust compiles until a build has
// produced those files once. Failing here would be a trap rather than a gate: the way
// to produce them is `dotnet run build`, and that command runs this gate first -- a
// fresh clone would never get past it.
//
Console.WriteLine("- Skipping the Rust tests and Clippy: the .NET sidecar or the downloaded libraries are missing, and Tauri's build script needs both before anything Rust compiles.");
Console.WriteLine(" Run 'dotnet run build --skip-verify' once. From then on, this part of the gate runs with the rest.");
}

results.Add(("Model sources", new VerifyModelsCommand().VerifyModels()));

Console.WriteLine("==============================");
Console.WriteLine("- Quality gate:");
foreach (var (what, exitCode) in results)
Console.WriteLine($" - {what}: {(exitCode is 0 ? "passed" : $"failed, exit code {exitCode}")}");

var failed = results.Count(result => result.ExitCode is not 0);
if (failed is 0)
{
Console.WriteLine($"- All {results.Count} checks passed.");
return 0;
}

Console.WriteLine($"- {failed} of {results.Count} checks failed.");
return 1;
}

/// <summary>
/// Whether a build has already produced the files Tauri's build script reads.
/// </summary>
/// <remarks>
/// Both are products of a build rather than of the repository: the .NET app arrives as a
/// sidecar, and the PDF library is downloaded into the resources. The other resource
/// directories the configuration names are in the repository and are always there.
/// </remarks>
/// <returns>True, when cargo can get past the build script.</returns>
private static bool WhatTauriExpectsIsThere()
{
var distributionDirectory = Path.Combine(Environment.GetAIStudioDirectory(), "bin", "dist");
if (!Directory.Exists(distributionDirectory) || !Directory.EnumerateFiles(distributionDirectory, $"{SIDECAR_PREFIX}*").Any())
return false;

var librariesDirectory = Path.Combine(Environment.GetRustRuntimeDirectory(), "resources", "libraries");
return Directory.Exists(librariesDirectory) && Directory.EnumerateFiles(librariesDirectory).Any();
}
}
Loading
Loading