SageFs 0.6.875

dotnet tool install --global SageFs --version 0.6.875
                    
This package contains a .NET tool you can call from the shell/command line.
dotnet new tool-manifest
                    
if you are setting up this repo
dotnet tool install --local SageFs --version 0.6.875
                    
This package contains a .NET tool you can call from the shell/command line.
#tool dotnet:?package=SageFs&version=0.6.875
                    
nuke :add-package SageFs --version 0.6.875
                    

<div align="center">

SageFs

You save. The affected tests re-run. The running app serves the new code.

F# Interactive that already has your project loaded, re-runs your tests on unsaved edits, and patches your running app. VS Code, Neovim, a browser dashboard and your AI agent all share the one session. Free, MIT.

NuGet .NET 10 License: MIT Tests Live testing

</div>

What is SageFs?

Hey, I'm Will. SageFs is the thing I wanted every time I sat there waiting on a rebuild just to find out whether one little function did what I thought it did.

F# Interactive is amazing. Here's what I wanted on top of it.

Type an expression, get the answer, no build step. FSI is one of the best things about F# and I use it all day. I'm not trying to replace it, SageFs is FSI underneath. But everybody who's used dotnet fsi on a real project has run into the same things:

  • It starts empty. Your project, your packages and your opens are all on you, through #r and #load, every time.
  • It's one process in one terminal. A bad eval or a hang takes the session with it, and nothing else can use it while you do.
  • It knows nothing about your tests. Change a function, run dotnet test, wait.
  • It can't touch a running app. Edit a handler in a web app and it's stop, build, start, click back to where you were.
  • Values scroll away. You see val x : int = 3 once, then it's gone off the top of the terminal.
  • Only you can drive it. An AI agent can't sit in your REPL, so it shells out to dotnet build like it's 2015.

What SageFs does about it

Start sagefs once. Then:

On a real project, FSI With SageFs
It starts empty A session loads your real project, built by the SDK dotnet picks in that folder, in its own host process. Your package versions never fight mine. No config files, and it works on half-written code.
One process, one terminal One daemon, any number of isolated session workers. VS Code, Neovim, the dashboard and agents all talk to the same live session at the same time, and each keeps its own view.
It knows nothing about your tests Edit a function, saved or not, and the affected tests re-run against it. The failing one goes red in your gutter and green again when you fix it, without touching the file on disk. Each line lists the tests that run it, each result says whether it ran against evaluated code or a real build, you can pause it or scope it to some tests, and in VS Code you can debug the failing test. When it can't tell what an edit affects it runs everything, it never reads an empty selection as green. See live testing.
It can't touch a running app Save a .fs file and SageFs re-points the changed methods in the process that's already running, including inline lambdas in a route list, instance members, and code you added, removed or gave a new signature. Your state stays where it was. Generic functions and a new member on an existing type restart the app, and so does anything else it can't patch, and it says why. See hot reload.
Values scroll away Every binding in your session is shown live in the dashboard, top-down, and updates after each eval.
Only you can drive it An MCP server with 61 tools. An agent evals, type-checks and runs your tests in the same session you're looking at, and run_tests hands back a receipt so a stale pass never counts as green.

Most of the live testing and hot reload rows above merged on 2026-10-01 and are in the next release. They are not in v0.6.870. The progress page says which entry is which.

For the curious, there are three short write-ups on how it's done, each with the tools I learned from, what F# and .NET don't hand you, how SageFs gets it done, and where Microsoft's version is ahead: how hot reload works, how live testing works, and how SageFs opens projects plain FSI can't.

If you want to see how it got here, what SageFs has become, stretch by stretch goes through each change as before, now, why it matters, and a link to the code.

I'm not going to pretend all of that is finished. Live testing and hot reload each have documented limits, written down next to the tests that pin them. But all of it runs today, and none of it is a mockup.

It runs as a daemon with isolated session workers, so editors, dashboard tabs, and MCP clients all share live state at the same time.

How's it different from Ionide? Ionide gives you the editor smarts (IntelliSense, diagnostics, project support) through the F# Compiler Service, and it's great. SageFs adds live execution: eval any expression and see the result inline, continuous test feedback on every save (or keystroke, if you want it), and hot reload that patches your running app. Use both together: Ionide for editing, SageFs for running. They get along fine.

It runs on Windows, macOS, and Linux. SageFs itself installs with the .NET 10 SDK. Your projects aren't stuck on that version though. Each session's host gets built with whichever SDK dotnet picks in your project's folder, and runs on that SDK's runtime. So a global.json pin is honored, and without one you get the newest SDK you have installed. If you want a project on .NET 10 while an 11 preview is also installed, pin it with global.json.

This started as an experiment in how far agentic development could go, and it's grown into the tool I use every day. It's moving fast, it's got rough edges, and you WILL find things you wish worked differently. That's exactly the feedback I want. Feel free to submit issues or PRs, and if you really need to get ahold of me, Discord is the most reliable way, same name there too.

Table of Contents

๐Ÿ†• Never written F#? You're in the right place.

Pick your language. Each guide maps familiar concepts to F#, with runnable examples that show results as soon as you press Alt+Enter.

๐Ÿ Python ยท ๐Ÿ““ Jupyter ยท ๐Ÿ”ท C# ยท โ˜• Java ยท ๐ŸŸจ JS/TS ยท ๐Ÿฆ€ Rust ยท ๐Ÿง˜ F# Koans

Or just dive in: dotnet tool install --global SageFs && sagefs, then open any .fsx file and hit Alt+Enter.


Key Features

โšก Hot Reload

Save a .fs file and SageFs figures out which functions changed and uses Harmony to re-point those methods in the process that's already running. No rebuild, no restart, and yes, that includes apps whose route table was only ever built once at startup. Connected browsers refresh automatically over SSE.

Because it re-points methods, not everything is patchable: a handler that's called per request reloads, a handler whose output was computed once at startup can't. Prefer let getHome (ctx: HttpContext) = ... over let getHome : HttpHandler = Response.ofHtml (pageLayout []). Lambdas in your route list, instance members, added or removed functions and a changed signature all patch in place now. Reshaping a type, adding a member to an existing type, a generic function, and a let mutable whose type changed restart the app and say why instead of pretending to reload.

Apps started by a .SageFs/init.fsx that #loads your sources patch in place too, on .NET 10 and .NET 11. SageFs tracks which copy of a function the app is actually holding and patches that one.

How you start the app matters, and I'd rather tell you than have you find out. An app started from FSI or an init.fsx lives in the same process as the reload agent, so a save patches it in place and its state stays put. An app started with run_app runs in the worker, out of the agent's reach, so a save restarts it with your change and says why (about six seconds for the ticker demo on my machine). Until 0.6.845 that second path reported Patched and changed nothing, which I caught by editing the demo and watching the output not move. It's fixed, and there's a test that starts an app with run_app, saves an edit and checks what the running app prints. The table and the source links are in docs/hot-reload.md.

A detour landing isn't reported as live. A patched function stays PatchPending (the browser still refreshes, because the change may well be live) until its new body has been seen running, and then it becomes Patched. If ten seconds go by and it never ran, it's NeverEntered, and the message says to exercise it. An app nobody has clicked on yet looks exactly like that, so the wording stays at "unconfirmed". So does a function the JIT inlined into its caller, but only in a project built with optimizations. SageFs builds a session with -p:Optimize=false, where an F# inline call stays a call and the assembly is marked so the JIT does not inline either. If it finds an optimized build (a Release you built by hand), it marks the session Degraded and says so, because a patch can be bypassed there and nothing else would tell you. Nothing is claimed that wasn't observed.

Your app's live state survives a save. A let mutable you didn't touch keeps its value, private ones included. Edit a mutable's initializer and the app keeps its live value, SageFs tells you what it kept, and the dashboard's Hot Reload panel (or the reset_hot_reload_state MCP tool) has a Reset for when you want the new initializer to run. Redefine a plain let value and it gets its new value, as long as nothing in the running app kept a copy of the old one. The app tells SageFs where every read of it went, so if startup put it in a closure, or a lazy cached it, or a handler that hands it on already ran, it's a restart that names who kept it, never a fake Patched. The details, and where that falls short, are in docs/hot-reload.md.

Curious how this is possible on F#? How hot reload works covers the Erlang, Clojure and Smalltalk ideas it borrows, what .NET doesn't give F# by default, and how it compares with .NET's own hot reload.

docs/hot-reload.md is the authority. It carries the full what-reloads / what-restarts table, each row pinned by an executable test. This README deliberately doesn't duplicate it, so the two can't drift apart. (No test measures reload latency, so no figure is quoted here.)

๐Ÿค– AI Agent Support

SageFs exposes a Model Context Protocol server with an affordance-driven state machine: the full tool catalog is always listed, but calling a tool that doesn't apply to the current session state gets rejected with a structured error instead of a raw failure. get_daemon_status reports daemon health, and get_session_status reports the selected session and its available tools. The core MCP path is session trust, F# evaluation, running tests, and failure explanation. run_tests asks the same live-testing engine the dashboard and the editors use and hands back a receipt: a pass left over from an earlier run never counts, and Incomplete is not green. Copilot, Claude, and any MCP client can execute F# code, type-check it, verify a changed behavior, and run tests against your real project.

Agents left alone will happily pile on complexity. Give one a fast, type-checked REPL with tests re-running on every change and it gets caught the same way I do, right away.

If you're using an agent, read docs/agents.md first and install the SageFs skill. An agent that doesn't know the rules goes straight back to dotnet build, wait, dotnet test, wait, and you lose the whole point. The skill makes the REPL its inner loop. The page also shows how to pull an agent back when it drifts: a back_to_the_repl prompt, and a Claude Code hook that stops mid-task builds.

๐Ÿ–ฅ๏ธ One Daemon, Every Client

Start SageFs once, then connect from VS Code, Neovim, the web dashboard, or an MCP client. Open several at once and they share a live session, with each client keeping its own selection. Pair with your agent, watch it work in the dashboard, keep typing in Neovim, all at the same time.

flowchart TB
    D[SageFs Daemon]

    D --- VS[VS Code]
    D --- NV[Neovim]
    D --- WB[Web Dashboard]
    D --- AI[MCP Clients]
    D --- JP[Jupyter Kernel]

    style D fill:#1a1b26,stroke:#7aa2f7,stroke-width:2px,color:#c0caf5
    style VS fill:#1a1b26,stroke:#9ece6a,color:#c0caf5
    style NV fill:#1a1b26,stroke:#9ece6a,color:#c0caf5
    style WB fill:#1a1b26,stroke:#7dcfff,color:#c0caf5
    style AI fill:#1a1b26,stroke:#e0af68,color:#c0caf5
    style JP fill:#1a1b26,stroke:#bb9af7,color:#c0caf5

Get Started

The short version, for people who read the last page of the book first:

  1. dotnet tool install --global SageFs, then make sure ~/.dotnet/tools is on your PATH.
  2. sagefs check tells you what's missing, with a fix next to each failure.
  3. Run sagefs in a terminal and leave it there, or run it as a service.
  4. Open http://localhost:37750/dashboard, or point your editor or agent at it (step 4 below).
  5. sagefs status says whether the daemon is up. Exit code 1 means it isn't.

1. Install SageFs (30 seconds)

You need the .NET 10 SDK or the .NET 11 SDK. The tool ships a build for each and dotnet picks the one that matches yours. Nothing else.

dotnet tool install --global SageFs

To update later: dotnet tool update --global SageFs. If that says "already installed" when you know a newer version is out, add --version X.Y.Z โ€” dotnet tool update resolves through NuGet's search index, which lags the package store by a few minutes.

2. Check your environment (optional)

sagefs check

Validates .NET SDK, FSI, project files, port availability, and daemon state. Actionable hints on every failure. Skip this if you've used SageFs before.

3. Start the daemon

sagefs

SageFs runs in the foreground, streaming daemon logs to that terminal. It's not an F# REPL by itself. Leave it running, then create a session for YourProject.fsproj from your editor, MCP client, or the dashboard.

No project? Just run sagefs with no arguments. The daemon starts bare and waits for clients. Your editor will create sessions on demand.

SageFs writes <project>/.SageFs/warmup-replay-cache.json the first time it warms a project up, and a .gitignore right beside it. That cache is machine-generated and ignores itself, so your git status stays clean. config.fsx and init.fsx in the same folder are yours. Those two you commit.

Run it as a service (Linux)

The daemon that sagefs mcp starts for an agent is owned by that agent (see how long the daemon lives). When the agent exits, the daemon, its sessions and the dashboard go with it. If you'd rather have one that's just there, let systemd babysit it:

mkdir -p ~/.config/systemd/user
cp contrib/systemd/sagefs.service ~/.config/systemd/user/
systemctl --user enable --now sagefs

contrib/systemd/sagefs.service is a user unit. It runs ~/.dotnet/tools/sagefs --supervised, restarts it on failure, and starts it at login (loginctl enable-linger $USER if you want it at boot). systemctl --user status sagefs shows it, journalctl --user -u sagefs has its logs. When an agent then runs sagefs mcp, it finds your daemon already up and just bridges to it, so nothing it does can take the daemon down.

4. Connect your editor

VS Code: Install SageFs from the Marketplace or Open VSX (or the .vsix from Releases), open an F# file, and press Alt+Enter on any expression. The result appears inline.

Neovim: Add "WillEhrendreich/sagefs.nvim" to your plugin manager. Press Alt+Enter to evaluate. See Neovim setup.

Web dashboard: Open http://localhost:37750/dashboard for session management, evaluation, output, test state, and diagnostics without an editor extension.

AI agent (Claude Code, Copilot, Codex, Cursor, anything that speaks MCP): claude mcp add sagefs -- sagefs mcp (or your client's equivalent for a stdio server). It starts the daemon for you if one isn't already running, so there's no ordering to get wrong. A daemon started that way lives as long as that agent session does. Start sagefs yourself first (or run it as a service) if you want it to stick around. claude mcp add defaults to local scope, meaning this project only. Add -s user to get it everywhere. Then install the SageFs skill. Without the skill your agent will iterate with dotnet build and never touch the REPL. docs/agents.md has the one-line install, an AGENTS.md snippet for other agents, and what to do when an agent drifts. Clients that only speak HTTP can still point at http://localhost:37749/ โ€” see docs/mcp-tools.md.

5. Enable live testing

Live testing runs as you type. SageFs evals your edited buffer into the session and re-runs only the affected tests against the new code. An unsaved edit flips a failing test red and back to green when you fix it, without ever touching the file on disk. Results stream inline with source-mapped gutter markers and coverage. All five frameworks (Expecto, xUnit v2 and v3, NUnit, MSTest, TUnit) discover, run, and report with framework-specific messages.

When live testing is enabled and a project is loaded, edits (saved or unsaved) re-run the affected tests automatically and update gutter state. The engine, SSE events, coverage, and editor integrations work today across VS Code and Neovim.

6. What you'll see

  • Gutter markers: โœ“ green (passing), โœ— red (failing), โ—‹ gray (no coverage)
  • Inline results: Expression values appear to the right of your code
  • Coverage bars: Colored bars in the gutter show which lines are covered by tests
  • Failure details: Hover over red markers to see Expected vs Actual diffs

๐Ÿ’ก Tip: Use the SageFs: Mark All Tests Stale command (Command Palette) to re-run everything.

MCP (streamable HTTP):  http://localhost:37749/       โ† recommended for new MCP clients
MCP (legacy SSE):       http://localhost:37749/sse    โ† older MCP clients
Dashboard:              http://localhost:37750/dashboard

New to F#? You don't need any F# knowledge to start. Jump to the migration guide for your language: each one maps concepts you already know to F#, with runnable examples.

<details> <summary>Build from source</summary>

git clone https://github.com/WillEhrendreich/SageFs.git
cd SageFs
dotnet build && dotnet pack SageFs -o nupkg
dotnet tool install --global SageFs --add-source ./nupkg --no-cache

</details>

๐Ÿ“š Documentation at sagetech.dev/sagefs.

๐Ÿ“ Docs in this repo: the same guides and technical reference, alongside the code.


Three Workflows: REPL, Live Testing, and Hot Reload

๐Ÿ“– Full guide: Understanding Workflow Modes: decision tree, diagrams, real-world scenarios, troubleshooting, and how the Live Testing workflow differs from the live-testing toggle.

A session runs in exactly one workflow, and the set is closed: SessionWorkflow is Interactive | LiveTesting | HotReload. The tradeoff between the first two and the third comes from a physical constraint of the .NET runtime. I didn't build it that way just to be difficult.

REPL (Interactive, the default) gives you a full interactive F# session. You can redefine types, experiment freely, and iterate on designs. This is what you want when you're prototyping domain types, exploring APIs, or working through a problem interactively.

Live Testing (LiveTesting) is the same full REPL, plus SageFs turns live testing on for you the moment the session is ready, and affected tests re-run on debounced keystrokes rather than on save. Pick it when you're doing TDD and want the test loop running without arming it by hand.

Hot Reload (HotReload, also spelled live / weblive / web on the command line, for historical reasons) enables browser hot reload. Save a .fs file and connected browsers update via SSE, with no manual refresh. To make this work, SageFs uses runtime patching to inject code changes into the running app. That patching requires a single-assembly FSI mode, which means you cannot redefine types (you'll get FS0037 errors). Expressions, function bodies, and let bindings work fine.

REPL (default) Live Testing Hot Reload
Type redefinition โœ… Full โ€” redefine types freely โœ… Full โŒ FS0037 โ€” expression-level changes only
Browser hot reload โŒ Manual refresh required โŒ Manual refresh required โœ… โ€” see docs/hot-reload.md for which code shapes patch and which need a restart
Live testing โœ… Available โ€” you turn it on โœ… On automatically when the session is ready โœ… Available โ€” you turn it on
Best for Prototyping, domain modeling, exploration TDD, red-green loops Web apps with Falco, Datastar, ASP.NET

Live testing is also a per-session toggle that works in any of the three workflows (POST /api/live-testing/enable, or your editor's Enable Live Testing command). The LiveTesting workflow is the shortcut that arms it for you and drives it from keystrokes instead of saves. So "which workflow" and "is live testing on" are two different questions.

Choosing the right workflow

  • Building a web app with Falco.Datastar, Giraffe, or any ASP.NET pipeline? Use Hot Reload. You want save-and-see-it feedback in the browser.
  • Writing tests first? Use Live Testing. The loop is armed for you and runs as you type.
  • Exploring types, designing domain models, or working in .fsx scripts? Use REPL. You need the freedom to reshape types as you go.
  • Not sure? Start with REPL. Switch when you need the browser or the test loop.

Switching workflows

Use your editor's command to switch workflows:

  • Neovim: :SageFsWorkflow live or :SageFsWorkflow repl. The plugin's command documents only those two, so reach for MCP if you want livetesting
  • VS Code: Command Palette โ†’ SageFs: Switch Workflow. This hits POST /api/sessions/{sid}/workflow directly, which restarts the same session id in place
  • MCP: switch_workflow with target = repl | livetesting | live (โš ๏ธ live means Hot Reload, not live testing; the alias predates the third workflow). This one creates a new session in the target workflow and stops the old one
  • Web dashboard: a real dropdown next to your session now, not a read-only badge. Pick a workflow and it switches, restarting the same session id in place, same as VS Code

VS Code and the dashboard swap the worker under your existing session id (spawn-first, so there's no dead window while it happens); the MCP tool spins up a fresh session and retires the old one. Either way, REPL-defined bindings are lost on the switch. Persisted files are unaffected.

Auto-detection

When SageFs detects web-oriented packages in your project (Falco.Datastar, Giraffe, Saturn, etc.), it suggests switching to the Hot Reload workflow. It's a suggestion in the tool's response text. SageFs never switches on its own.


How SageFs Works

SageFs has exactly three concepts: a daemon, sessions, and clients.

flowchart TB
    subgraph D[SageFs Daemon - one per machine]
        S1[Session Worker 1 - MyApp]
        S2[Session Worker 2 - Tests]
        S3[Session Worker 3 - Bare FSI]
        SVC[MCP / Dashboard / File Watcher / Hot Reload]
    end

    D --- VS[VS Code]
    D --- NV[Neovim]
    D --- WB[Web Dashboard]
    D --- AI[AI Agent - MCP]

    style D fill:#1a1b26,stroke:#7aa2f7,stroke-width:2px,color:#c0caf5
    style S1 fill:#1a1b26,stroke:#9ece6a,color:#c0caf5
    style S2 fill:#1a1b26,stroke:#9ece6a,color:#c0caf5
    style S3 fill:#1a1b26,stroke:#9ece6a,color:#c0caf5
    style SVC fill:#1a1b26,stroke:#e0af68,color:#c0caf5
    style VS fill:#1a1b26,stroke:#bb9af7,color:#c0caf5
    style NV fill:#1a1b26,stroke:#bb9af7,color:#c0caf5
    style WB fill:#1a1b26,stroke:#bb9af7,color:#c0caf5
    style AI fill:#1a1b26,stroke:#bb9af7,color:#c0caf5

The daemon is a service. It starts with no project and no session. It just listens, and clients tell it what to do.

Sessions are isolated workers. Each session is a separate OS process with its own FSI instance, project, and file watcher, so they can't interfere with each other. Create as many as you need. The watcher is rooted at the session's working directory, and a session opened in your home directory or a filesystem root is refused a watcher and says so in /health (details). Open the session in the project directory.

Clients are thin. Editor integrations, dashboard tabs, the Jupyter bridge, and MCP clients all connect to the same daemon. They create sessions, send code, and read results. Multiple clients can share the same session or each use their own.

The workflow:

  1. Start the daemon: sagefs
  2. A client (editor, Jupyter, dashboard, AI) creates a session: POST /api/sessions/create with a project path
  3. The daemon spawns a worker, loads the project, starts watching files
  4. The client sends code, reads diagnostics, runs tests, all through the daemon
  5. Other clients can connect to the same session simultaneously

This means the daemon doesn't need to know your project at startup. It starts bare and waits for clients to create or attach to sessions.


What You Get in Each Editor

Every frontend connects to the same daemon. Open several at once and they all see the same state.

Capability VS Code Neovim Web Dashboard MCP
Eval code / file / block โœ… โœ… โœ… โœ…
Inline results โœ… โœ… โœ… โœ…
Live diagnostics (SSE) โœ… โœ… โœ… โœ…
Hot reload controls โœ… โœ… โœ… โœ…
Session management โœ… โœ… โœ… โœ…
Code completion โœ… โœ… โ€” โ€”
CodeLens โœ… โœ… โ€” โ€”
Live test gutters โœ… โœ… โ€” โ€”
Coverage gutters โœ… โœ… โ€” โ€”
Failure narratives โœ… โœ… โœ… โœ…
Test source-jump โœ… โœ… โ€” โ€”
Debug a failing test โœ… โ€” โ€” โ€”
Test panel โœ… โœ… โœ… โœ…
Test policy controls โœ… โœ… โœ… โ€”
Type explorer โœ… โœ… โ€” โ€”
Call graph โœ… โœ… โ€” โ€”
History browser โœ… โœ… โœ… โœ…
Test trace โœ… โœ… โœ… โ€”

A โœ… in the MCP column means a tool in the 61-tool surface does it. Five rows used to claim โœ… and didn't have one, so I fixed the row instead of the code, since the code was already the right call: completions and the type explorer are FSharp.Compiler.Service features the editors call over HTTP (the get_completions / explore_type members in SageFs/McpTools.fs carry a [<Description>] but no [<McpServerTool>], so they aren't exposed at all); the call graph is GET /api/dependency-graph (the MCP plan_ripple / get_cell_dependencies tools graph FSI cells, not source symbols); run policy is POST /api/live-testing/policy only; and there is no test-trace tool. docs/LIVE_TESTING_GUIDE.md says so in as many words. The columns other than MCP say what's wired, not what's tested: most of the editor-side rendering (gutters, CodeLens, decorations, tree views) currently has no automated coverage in either client.

<details> <summary><strong>Editor setup guides</strong></summary>

VS Code

Install SageFs from the VS Code Marketplace or Open VSX, or the .vsix in Releases. Written in F# via Fable, not TypeScript.

Current wiring includes Alt+Enter eval, CodeLens, live test decorations, native Test Explorer integration, hot reload sidebar, session context, type explorer, call graph, event history, dashboard webview, status bar, auto-start, Ionide command hijacking, coverage gutter bars, inline failure decorations, failure narrative enrichment, and test source-jump.

Neovim

sagefs.nvim: 62 Lua modules, 55 commands, 1400+ tests.

-- lazy.nvim
{ "WillEhrendreich/sagefs.nvim", ft = { "fsharp" }, opts = { port = 37749, auto_connect = true } }

Features: Cell eval, inline results, gutter signs, SSE live updates, live test panel, coverage panel with per-file breakdown, type explorer, call graph, history browser, session export to .fsx, code completion, branch coverage gutters, filterable test panel, display density presets, combined statusline component, Telescope source-jump (<CR>), failure narrative floating window (<C-d>), and SSE-driven test state caching.

AI Agent (MCP)

SageFs exposes 61 MCP tools, from send_fsharp_code to targeted_verify to list_tests. All of them are listed all the time; calling one that doesn't apply to the current session state gets rejected with a structured error rather than being hidden. get_daemon_status reports daemon health, and get_session_status reports the selected session and its available tools. Any MCP client can connect. See the full MCP Tools Reference for the complete list and per-client configuration examples.

Streamable HTTP (recommended: auto-reconnects, no session drops):

{ "mcpServers": { "sagefs": { "type": "streamable-http", "url": "http://localhost:37749/" } } }

SSE (legacy clients that don't support Streamable HTTP yet):

{ "mcpServers": { "sagefs": { "type": "sse", "url": "http://localhost:37749/sse" } } }

OpenCode: Add to ~/.opencode.json:

{
  "mcp": {
    "sagefs": {
      "type": "remote",
      "url": "http://localhost:37749/sse",
      "enabled": true
    }
  }
}
Web Dashboard / Jupyter
sagefs --jupyter conn.json  # Run as a Jupyter kernel (experimental)
# Dashboard auto-starts at http://localhost:37750/dashboard

The Jupyter kernel is experimental. Its wire-protocol message shapes and HMAC signing are unit-tested, but nothing in the suite opens a ZMQ socket or launches sagefs --jupyter, so the transport (SageFs/JupyterTransport.fs, NetMQ) is unproven end to end. The dashboard isn't experimental: it has real browser journeys in CI.

</details>


โŒจ๏ธ Keybindings Across Editors

Action VS Code Neovim
Evaluate selection/cell Alt+Enter <M-CR> (or <leader>re)
Evaluate entire file Alt+Shift+Enter <leader>rf
Cancel evaluation Ctrl+Shift+C <leader>rx
Clear inline results Command Palette <leader>rc
Run all tests Command Palette <leader>rT
Toggle test panel Command Palette :SageFsTestPanel
Jump to test source Click test in explorer <CR> in telescope
Show failure narrative Hover on red marker <C-d> in test panel
Session picker Command Palette <leader>rs

Full keybinding references: VS Code ยท Neovim. The Neovim plugin lives in its own repository, so this table is a copy. Its keymaps are authoritative there, and nothing in this repo verifies them. (Neovim maps everything under <leader>r, not <leader>s, which LazyVim reserves for Search.)


๐ŸŽจ Gutter Icons

Icon Meaning
โœ“ (green) Test passing โ€” this code is covered by at least one passing test
โœ— (red) Test failing โ€” a test covering this code has failed
โ—‹ (gray) No coverage โ€” no test exercises this line
โ”‚ (green bar) Coverage healthy โ€” all tests covering this line pass
โ”‚ (red bar) Coverage degraded โ€” some tests covering this line are failing
โ”‚ (gray bar) Not covered โ€” no test reaches this line
โŠ˜ Inline failure โ€” shows the test name and Expected/Actual diff

๐Ÿ’ก Hover over any gutter icon for details. In Neovim, press <C-d> on a failing test for the full failure narrative.

๐Ÿ› In VS Code, a failing test has a Debug lens and a Debug link in its hover, and the Test Explorer has Debug Test. It attaches the C# extension's .NET debugger to the process that runs your tests, so breakpoints bind in your compiled project code. Code you evaluated in the session has no debug symbols, so breakpoints in it will not bind. How it works and what it needs.


Live Testing Cost Comparison

Visual Studio Enterprise has Live Unit Testing for C#. It costs about $250/month per seat, it only works in Visual Studio on Windows, and it supports xUnit, NUnit and MSTest. It copies your repo to a private workspace and runs MSBuild on it for every change (Microsoft's description). Its pages don't mention F#.

SageFs delivers that loop with a REPL-centered architecture, and goes past it: an unsaved edit evals into the session and re-runs only the affected tests against your new code. No save, no copy of your repo, no MSBuild rebuild in the loop. Editors post the live buffer to POST /api/sessions/{sid}/buffer-changed; that endpoint has an integration test of its own, and the actual "an unsaved edit overrides the test a saved build already registered" behavior is proven at the worker level too (WorkerLiveTestEvalTests.fs), so the unsaved path is wired and covered now, not just wired. SageFs is F#-first and works across VS Code and Neovim. Client polish still varies, but the engine, SSE, and coverage are solid.

VS Enterprise Live Testing SageFs
How an edit is checked A private copy of the repo, built with MSBuild Your unsaved buffer is type-checked and evaluated in the FSI session that already has your project loaded. No copy, no MSBuild rebuild. Measured through the real daemon on the small F# sample: a verdict on the edited function's test arrives about 0.7 s after the edit leaves the client (p50 712 ms, p95 783 ms over 20 edits), and a save is back to all green in about 0.5 s (p50 542 ms, p95 631 ms over 20 saves), on a 16-thread Ryzen 7 5800XT running Linux. That is one machine and one small project, see the pipeline note below
Which tests cover a line Per-line glyphs. Hover for how many tests hit the line, select the glyph for the tests by name (Microsoft) Per-line, with the exact tests whose own recorded coverage reaches it. Tests of an instrumented project run one at a time to make that true. Visual Studio runs the tests in each assembly one after another too, per the same page
What a result ran against A real build of your solution Each row says: evaluated code, a real build, a real build that agreed, or a real build that disagreed and why. A real build of the saved text confirms the eval once editing goes quiet; an unsaved buffer stays evaluated
Debugging a failure The glyph's tooltip debugs the set of tests, or only the ones you select VS Code: a failing test's Debug lens, hover link and Test Explorer profile attach a .NET debugger. One test at a time, none in Neovim yet, and breakpoints don't bind in code the session evaluated
Pause and scope Pause, also on build, debug and low battery. A playlist edited from the tool window or a right-click. A memory cap Pause, and an include or exclude set by name pattern. No battery or debugger detection, no memory cap, no right-click include. Why
Code that doesn't compile Build errors go to the Output window Nothing is evaluated or run and your last results stay, marked as blocked, with the file and error count. Tree-sitter still finds where your tests are in broken code
Why those tests ran Impacted tests are detected, the mechanism isn't documented Every selection says why (exact dependency match, coverage approximation, fallback) and a failure says how long ago it last passed and what changed
Editors Visual Studio only VS Code ยท Neovim ยท Web dashboard ยท MCP clients
Frameworks MSTest ยท xUnit ยท NUnit + Expecto ยท TUnit ยท xUnit v3 ยท extensible
Price ~$250/month Free, MIT licensed

Where Visual Studio is still ahead: it debugs several tests at once, it runs every result against a real build (mine confirms after two quiet seconds, and never for an unsaved buffer), it pauses on battery and while you debug, it has a memory cap and a right-click playlist, and it has been shipping since 2017. The plan for each, or the reason I didn't copy it, is in how live testing works. The rows on tests per line, what a result ran against, debugging, and pause merged on 2026-10-01 and are in the next release, not in v0.6.870.

<details> <summary><strong>Three-speed feedback pipeline: how the sub-second path works</strong></summary>

<br />

  1. Tree-sitter detects test attributes in broken/incomplete code โ†’ immediate gutter markers
  2. F# Compiler Service type-checks โ†’ dependency graph, reachability annotations
  3. Affected-test execution via hot-eval โ†’ โœ“/โœ— results inline

Each stage is progressively slower and progressively more certain, so you get a marker before you get a verdict. I want to be straight about what's actually measured here: the per-stage millisecond figures that used to sit in this README were never measured, so I pulled them. What is measured now is the end to end path, not the stages: the --integration-lt tier starts a daemon, opens the FromCSharp sample, and times keystroke-to-verdict and save-to-green over 20 edits each after 2 warm-up edits (the figures are in the table above, with the machine they came from), and it fails if the 95th percentile passes 3 s. Those figures say nothing about a large solution. There is one real, currently-enforced millisecond budget on pure logic: CoverageViewTests.fs's "hot path is tight" test asserts 100 coverage-view projections over 200 tests complete in under 100ms, and it runs in the default suite, not gated behind anything. LiveTestingCycleTests.fs has a second one (cycleBenchmarkTests, [Benchmark]-tagged) that the default suite filters out and no CI stage runs. Both measure pure decision functions (no FSI, no compiler, no real test execution in the loop), so treat them as a floor on the domain logic, not a promise about wall-clock save-to-green latency. Any end-to-end speed number you see about SageFs is an anecdote until something gates it.

Tests are automatically categorized (Unit, Integration, Browser, Property, Benchmark, Architecture), each with its own run policy: unit and property tests run automatically by default, integration/browser/architecture run on demand by default, and benchmarks stay disabled until you turn them on. All of this is configurable. SageFs's own suite leans hard on property-based testing: 707 property-based tests exercise the binary format, state machines, and event folds against generated inputs (grep -rho -E "\b[pf]?testProperty(WithConfig)?\b" SageFs.Tests across all *.fs files, the same regex SageFs.Tests/TestCountBadge.fs uses to restamp this line; restamp with dotnet run --project SageFs.Tests -- --update-badge rather than hand-editing it).

</details>


Under the Hood

Hot Reload: File changes are detected, the changed functions are re-emitted into FSI, and Harmony re-points those method pointers at runtime. Connected browsers auto-refresh via SSE. Which shapes patch, and which need a restart โ†’

Multi-Session: Run multiple isolated F# sessions simultaneously, each in its own worker sub-process with independent FSI, project, and file watcher. Full details โ†’

MCP Tools: 61 tools for session trust, code execution, running and listing tests, verification, failure explanation, analysis, and local friction reporting. They're affordance-gated at call time: the list is always complete, but a call to a tool that doesn't apply to the current session state is rejected with a structured error. Full reference โ†’

SSE Events: All editors receive test_source_locations, file_annotations, and failure_narratives events tagged with SessionId. Full reference โ†’

Architecture: Daemon-first design with isolated worker sub-processes, a web dashboard, editor integrations, and an affordance-driven MCP surface. Full details โ†’

Repository Map โ€” where things live

  • SageFs.Core/, the shared engine and runtime logic: session management, MCP/session operations, live testing, persistence, and shared rendering primitives
  • SageFs/, the CLI entrypoint, daemon host, MCP server, dashboard, and worker HTTP transport
  • SageFs.Host/, the worker process the daemon spawns per session: it loads your project with MSBuild, shadow-copies and instruments the outputs for coverage, and runs the worker HTTP transport the daemon talks to. It never runs your code
  • SageFs.FsiHost/, the isolated FSI host, built and launched per session by SageFs.Core/IsolatedFsiSession.fs. Sessions run in it by default. It holds the FSI session and your running code, the hot-reload detours and the live-testing agent. Its dependency closure is deliberately tiny (the SDK's own FSharp.Core and F# compiler, a renamed copy of Harmony, and about twenty of our own source files compiled in), and it references none of SageFs's assemblies, so a project's own dependency versions never collide with the daemon's. How that works
  • SageFs.Simulation/, deterministic simulation (DST) models that fold the real cores: file-reload routing, worker lifecycle, supervision, the manifest
  • SageFs.Tests/, the Expecto suite: unit tests, property tests, snapshot tests, the DST drivers, and every real-daemon integration and browser journey
  • sagefs-vscode/, VS Code extension (F# via Fable โ†’ JavaScript)
  • docs/, user docs, architecture notes, troubleshooting, and feature references
  • quality/, the release Definition-of-Done matrix the publish workflow gates on
  • samples/, runnable sample apps and language-onramp projects
  • scripts/, repo helper scripts and smoke/integration utilities
  • ci-pipeline.fsx, CI is one Fun.Build pipeline; the GitHub workflows just invoke it

SageFs.slnx covers the core tool, retained legacy projects, tests, and samples. The VS Code integration lives alongside it in sagefs-vscode/ because it uses its own packaging toolchain and release flow.

The Neovim plugin isn't in this repo. It lives in the separate sagefs.nvim repository.

If you're tracing the live testing / "test as you type" stack, start here:

  • Engine, discovery, dependency graph, and coverage: SageFs.Core/Features/LiveTestingExecutors.fs, LiveTestingTypes.fs, CoverageInstrumenter.fs, TestDiscovery.fs, TestTreeSitter.fs
  • Daemon routes, watchers, and SSE emission: SageFs/DaemonMode.fs, SageFs/McpServer.fs, SageFs/McpTools.fs
  • VS Code client wiring: sagefs-vscode/src/Extension.fs, LiveTestingListener.fs, TestControllerAdapter.fs, FileAnnotationsListener.fs
  • Neovim client wiring: the separate sagefs.nvim repo

<details> <summary><strong>๐Ÿ›ก๏ธ Supervised Mode: restart automatically on crash</strong></summary>

<br />

sagefs --supervised

Erlang-style supervisor with exponential backoff (1s โ†’ 2s โ†’ 4s โ†’ max 30s). After 5 consecutive crashes within 5 minutes, it reports the failure. Watchdog state exposed via /api/system/status and shown in the VS Code status bar. Use this when leaving SageFs running all day.

</details>

<details> <summary><strong>โšก Spawn-First Restart: no dead window on hard reset</strong></summary>

<br />

A hard reset spawns the replacement worker first and only retires the old one once the new one is ready, so the session is never left without a live worker mid-swap. (rebuild=true still runs a dotnet build before the swap; rebuild=false reuses the current build.)

</details>

<details> <summary><strong>๐Ÿ’พ Binary Persistence: instant resume</strong></summary>

<br />

SageFs persists the daemon's session registry and per-session test caches to compact binary files for near-instant cold starts. No JSON parsing, no database, just raw binary with CRC-32C integrity checking.

  • Daemon manifest (.sagefm, v1): the durable session registry (which sessions existed, their projects, and working directories, plus which was active), replayed on startup to rebuild your sessions.
  • Test cache files (.sagetc, v1): test discovery results, outcomes, durations, and coverage bitmaps of affected tests.

Design: length-prefixed strings, section headers with byte-count envelopes, version negotiation, and field-level bounds checking prevent OOM from crafted inputs. The formats are verified by property-based tests covering format corruption, round-trips, and write isolation.

</details>

<details> <summary><strong>๐Ÿ“‹ CLI Reference</strong></summary>

<br />

Usage: sagefs [options]                Start daemon (bare by default)
       sagefs --supervised [options]   Start with watchdog auto-restart
       sagefs --jupyter <conn.json>    Run as Jupyter kernel
       sagefs check                    Check environment before first run
       sagefs stop                     Stop running daemon
       sagefs status                   Show daemon info
       sagefs sweep [--kill]           Reap daemons whose owner process is gone
       sagefs play <ledger.jsonl>      Replay a portable cohort ledger file offline

Daemon options:
  --no-resume            Skip restoring previous sessions on startup
  --prune                Mark all stale sessions as stopped, then exit
  --supervised           Auto-restart on crash (exponential backoff)
  --mcp-port PORT        Custom MCP port (default: 37749). The dashboard runs on this port + 1.
  --ttl DURATION         Self-terminate after DURATION (e.g. 30m, 1h, 90s) with no
                         live sessions and no MCP/SSE clients.

The daemon starts bare and waits for clients to create or connect to sessions.

Full options: sagefs --help

</details>

<details> <summary><strong>๐Ÿ”ง Configuration</strong></summary>

<br />

Per-directory config: .SageFs/config.fsx, evaluated as real F# by an isolated FSI host, never by the daemon itself (SageFs.Core/ConfigHost.fs):

{ DirectoryConfig.empty with
    Load = Projects ["src/MyApp.fsproj"; "tests/MyApp.Tests.fsproj"]
    AutoOpenNamespaces = false }

The DirectoryConfig record also has InitScript, DefaultArgs, IsRoot, and SessionName fields (SageFs.Core/DirectoryConfigTypes.fs), but today only two of the six fields do anything. I'd rather tell you that plainly than let you write config that silently gets ignored:

  • AutoOpenNamespaces = false skips warmup auto-opening of namespaces and modules. This is honored everywhere, because every client's session-creation path bottoms out in the one place that reads it (SageFs/DaemonMode.fs:297).
  • Load picks which projects or solution a session loads, but only the web dashboard's own Create-session flow reads it (SageFs/DashboardTypes.fs:1119). MCP and the editor HTTP API take an explicit project, solution, or bare target and never consult this file, so Load has no effect on sessions created from an editor or an agent.
  • InitScript, DefaultArgs, IsRoot, and SessionName are parsed and stored but not wired to anything yet. Setting them has no effect today.

Built-in ways to create or edit the auto-open setting:

  • Dashboard: enter a working directory, then click Disable Warmup Auto-Open
  • VS Code: run SageFs: Configure Warmup Auto-Open
  • Neovim: run :SageFsConfig

If .SageFs/config.fsx doesn't exist, these affordances create it with:

{ DirectoryConfig.empty with
  AutoOpenNamespaces = false
}

If the config already exists, SageFs opens or points you at the file instead of overwriting your existing settings.

Startup profile: not the InitScript field above, and not global. At the end of a session's warmup, SageFs looks for .SageFs/init.fsx or .SageFsrc in that session's own working directory and evaluates it if found (SageFs.Core/StartupProfile.fs). There is no ~/.SageFs/init.fsx home-directory startup profile; no code path looks there.

Precedence: Inside the dashboard's Create-session flow only: an explicit project list you type wins, then .SageFs/config.fsx's Load, then auto-discovery from the working directory. MCP and the HTTP API (VS Code, Neovim) don't read the config file on session creation at all, so there's no precedence to speak of there. Whatever project list the client sends is used as-is.

</details>

โ“ Troubleshooting

Quick start: Run your editor's health check first (VS Code: Ctrl+Shift+P โ†’ "SageFs: Check Health" ยท Neovim: :checkhealth sagefs).

Problem Quick Fix
"SageFs daemon not found" dotnet tool install --global SageFs, then sagefs status
Results don't match the code you just wrote, or a "type not found, Version=โ€ฆ" / "Could not load file or assembly 'System.Runtime, Version=โ€ฆ'" error Your daemon is older than your code and is still serving what it started with. sagefs status to see its version, then dotnet tool update --global SageFs and restart it.
Port already in use sagefs stop or --mcp-port 8080
Wrong project selected "SageFs: Switch Project" in command palette
Stale REPL after code changes Save the file first โ€” source edits auto-reload. Use hard reset only for .fsproj / package changes.
Session stuck warming up on a big repo Name one project or solution, or choose a bare session explicitly. SageFs builds missing generated state itself. See Large repos

๐Ÿ“– Full Troubleshooting Guide โ†’: covers first-run issues, runtime problems, where the logs are, what a Degraded health reading means, platform-specific fixes, and diagnostic tools.

โš™๏ธ Configuration โ†’: every SAGEFS_* environment variable, with its default and what it is for.

๐Ÿ“Š Feature Matrix โ†’: compare features across VS Code, Neovim, the web dashboard, and MCP.


Coming from Another Language?

You don't need to know F# already. Find your background below for a guide that maps concepts you know to F#, with runnable examples.

Quick orientation: Every sample in /samples is a runnable .fsx script. Open it in a supported editor with SageFs connected, hit Alt+Enter on any expression, and results appear inline instantly.

Background One-liner Guide
๐Ÿ Python Same REPL energy, plus a compiler that catches bugs before you run Guide โ†’
๐Ÿ““ Jupyter Everything you love about notebooks, minus kernel crashes and JSON diffs Guide โ†’
๐Ÿ”ท C# Same .NET, same NuGet โ€” stop writing AbstractRepositoryFactoryImpl Guide โ†’
โ˜• Java Expressive, type-safe, concise โ€” what Java always wished it could be Guide โ†’
๐ŸŸจ JS/TS No undefined, no this bugs, no node_modules โ€” just functions Guide โ†’
๐Ÿฆ€ Rust Option, Result, pattern matching โ€” without the borrow checker Guide โ†’
๐Ÿง˜ F# Koans You already know F# โ€” now get instant feedback instead of dotnet run Guide โ†’

๐ŸŽฏ Visual Demos

See what SageFs makes possible beyond the REPL:

๐ŸŒ Reactive Web App โ€” Falco + Datastar, zero JavaScript

A full CRUD todo app in about 100 lines of F#. Edit a handler and save, and the browser updates immediately: no webpack, no bundler, no framework setup.

โ†’ samples/demos/webapp-datastar.fsx

The two Raylib demos below are unverified for hot reload. Hot reload itself works (see docs/hot-reload.md), but no automated test of any kind drives a Raylib window through a save, so the "updates live" claims here rest on nothing executable. I built them to show the shape of the thing, not as a proven guarantee. Treat them accordingly. Note also that the reload rules still apply: a frame loop reading a let mutable picks up nothing, because a mutable read compiles to a direct field load that no method detour can rewire. So starMaxSpeed-style tweaks need to be read through a function to reload.

๐ŸŽจ GPU Window โ€” Raylib Hello World with hot reload

A Raylib window intended to hot-patch on save: change the color, the text, or the animation, save, and it should update in the running window, no restart, no flicker.

โ†’ samples/demos/raylib-hello.fsx

๐Ÿ•น๏ธ Interactive Game โ€” live-tweakable physics

A playable star-catcher game. Editing starMaxSpeed, playerWidth, and starColors in the source file and saving is meant to apply to the running game without interrupting play.

โ†’ samples/demos/raylib-game.fsx


Contributing

SageFs is open source, and contributions are welcome: bug fixes, documentation improvements, new tests, or whole features. PRs are encouraged.

โ†’ Read the Contributing Guide for setup instructions, debugging workflow, coding standards, and how to make your first PR.

New to the codebase? Check the Good First Contributions section in the contributing guide for places where help is especially welcome.

License

MIT

Acknowledgments

SageFs exists because of Jo Van Eyck's fsi-mcp-server, a minimal F# Interactive MCP server that proved the concept of connecting FSI to editors via MCP. That project made everything here possible.

FsiX ยท sagefs.nvim ยท Falco & Falco.Datastar ยท Harmony ยท Ionide.ProjInfo ยท Raylib-cs ยท Fable ยท ModelContextProtocol

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed.  net11.0 is compatible. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

This package has no dependencies.

Version Downloads Last Updated
0.6.875 0 10/1/2026
0.6.870 35 10/1/2026
0.6.865 36 10/1/2026
0.6.864 36 10/1/2026
0.6.863 37 10/1/2026
0.6.860 37 10/1/2026
0.6.854 46 9/30/2026
0.6.851 57 9/30/2026
0.6.850 58 9/30/2026
0.6.849 53 9/30/2026
0.6.848 66 9/30/2026
0.6.847 58 9/30/2026
0.6.845 68 9/30/2026
0.6.844 49 9/30/2026
0.6.843 52 9/30/2026
0.6.842 64 9/30/2026
0.6.834 190 9/26/2026
0.6.833 100 9/26/2026
0.6.832 108 9/26/2026
0.6.831 112 9/25/2026
Loading failed