SageFs 0.6.875
dotnet tool install --global SageFs --version 0.6.875
dotnet new tool-manifest
dotnet tool install --local SageFs --version 0.6.875
#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.
</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#rand#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 = 3once, 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 buildlike 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
- Key Features
- Get Started
- Three Workflows: REPL, Live Testing, and Hot Reload
- How SageFs Works
- What You Get in Each Editor
- Keybindings
- Gutter Icons
- Live Testing Cost Comparison
- Under the Hood
- Repository Map
- Coming from Another Language?
- Visual Demos
- Contributing
- License
๐ 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.fsxfile 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: aback_to_the_replprompt, 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:
dotnet tool install --global SageFs, then make sure~/.dotnet/toolsis on yourPATH.sagefs checktells you what's missing, with a fix next to each failure.- Run
sagefsin a terminal and leave it there, or run it as a service. - Open
http://localhost:37750/dashboard, or point your editor or agent at it (step 4 below). sagefs statussays 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
sagefswith 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
.fsxscripts? 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 liveor:SageFsWorkflow repl. The plugin's command documents only those two, so reach for MCP if you wantlivetesting - VS Code: Command Palette โ
SageFs: Switch Workflow. This hitsPOST /api/sessions/{sid}/workflowdirectly, which restarts the same session id in place - MCP:
switch_workflowwithtarget=repl|livetesting|live(โ ๏ธlivemeans 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:
- Start the daemon:
sagefs - A client (editor, Jupyter, dashboard, AI) creates a session:
POST /api/sessions/createwith a project path - The daemon spawns a worker, loads the project, starts watching files
- The client sends code, reads diagnostics, runs tests, all through the daemon
- 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 />
- Tree-sitter detects test attributes in broken/incomplete code โ immediate gutter markers
- F# Compiler Service type-checks โ dependency graph, reachability annotations
- 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 primitivesSageFs/, the CLI entrypoint, daemon host, MCP server, dashboard, and worker HTTP transportSageFs.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 codeSageFs.FsiHost/, the isolated FSI host, built and launched per session bySageFs.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 worksSageFs.Simulation/, deterministic simulation (DST) models that fold the real cores: file-reload routing, worker lifecycle, supervision, the manifestSageFs.Tests/, the Expecto suite: unit tests, property tests, snapshot tests, the DST drivers, and every real-daemon integration and browser journeysagefs-vscode/, VS Code extension (F# via Fable โ JavaScript)docs/, user docs, architecture notes, troubleshooting, and feature referencesquality/, the release Definition-of-Done matrix the publish workflow gates onsamples/, runnable sample apps and language-onramp projectsscripts/, repo helper scripts and smoke/integration utilitiesci-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.nvimrepo
<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 = falseskips 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).Loadpicks 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, soLoadhas no effect on sessions created from an editor or an agent.InitScript,DefaultArgs,IsRoot, andSessionNameare 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
/samplesis a runnable.fsxscript. 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 mutablepicks up nothing, because a mutable read compiles to a direct field load that no method detour can rewire. SostarMaxSpeed-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
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 | Versions 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. |
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 |