From 59945d8899d13f237e782f0283ac97099c219b8d Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Thu, 5 Mar 2026 22:28:05 +0100 Subject: [PATCH] Add dotnet-fsi-interactive skill for .NET REPL via dotnet fsi Adds a skill teaching agents to use dotnet fsi as an interactive REPL for running .NET code without creating files. Includes eval scenario testing System.IO.Path edge cases. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/CODEOWNERS | 3 + .../skills/dotnet-fsi-interactive/SKILL.md | 98 +++++++++++++++++++ tests/dotnet/dotnet-fsi-interactive/eval.yaml | 27 +++++ 3 files changed, 128 insertions(+) create mode 100644 plugins/dotnet/skills/dotnet-fsi-interactive/SKILL.md create mode 100644 tests/dotnet/dotnet-fsi-interactive/eval.yaml diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 5bbde028c8..0b7ee1eba3 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -16,6 +16,9 @@ /plugins/dotnet/skills/csharp-scripts/ @dotnet/roslyn /tests/dotnet/csharp-scripts/ @dotnet/roslyn +/plugins/dotnet/skills/dotnet-fsi-interactive/ @dotnet/fsharp @T-Gro +/tests/dotnet/dotnet-fsi-interactive/ @dotnet/fsharp @T-Gro + /plugins/dotnet/skills/dotnet-pinvoke/ @dotnet/appmodel /tests/dotnet/dotnet-pinvoke/ @dotnet/appmodel diff --git a/plugins/dotnet/skills/dotnet-fsi-interactive/SKILL.md b/plugins/dotnet/skills/dotnet-fsi-interactive/SKILL.md new file mode 100644 index 0000000000..cc9f853c42 --- /dev/null +++ b/plugins/dotnet/skills/dotnet-fsi-interactive/SKILL.md @@ -0,0 +1,98 @@ +--- +name: dotnet-fsi-interactive +description: "Run and test .NET code interactively without creating files. Explore .NET APIs, verify runtime behavior, experiment with code in a persistent REPL (Read-Eval-Print Loop) session via dotnet fsi. Also for data analysis and inspecting stateful objects." +--- + +# F# Interactive REPL + +`dotnet fsi` — a REPL (Read-Eval-Print Loop) for .NET. **Single persistent process**: start once, send submissions repeatedly, state accumulates. + +## Inputs + +| Input | Required | Description | +|-------|----------|-------------| +| .NET SDK | Yes | Any version with `dotnet fsi` | + +## Sweet spots + +- **API exploration** — reflect on types, list members, try overloads, `#help` for inline docs +- **Runtime behavior verification** — test type assignability, generic variance, serialization edge cases +- **Database inspection** — connect once, query without reconnecting +- **Iterative analysis of slow-to-retrieve data** — fetch once, slice and explore from different angles +- **NuGet experimentation** — `#r "nuget: Pkg"` to pull in any package on the fly + +## When not to use + +- Single script run (`dotnet fsi file.fsx`) suffices +- Task needs a full project with multiple files + +## Workflow + +### Step 1: Start session + +```bash +dotnet fsi --nologo +``` + +Launch as async/background process, wait for `>`. `--nologo` suppresses banner noise. **Keep this process running for all subsequent steps.** + +### Step 2: Send submissions, read results + +A submission ends with `;;` and can contain **multiple expressions and let-bindings**. Batch related work into a single submission to minimize round-trips — every read echoes the full session, so fewer submissions = less token overhead. + +```fsharp +open System;; +let minDate = DateTime.MinValue;; +printfn "MinValue: %A, DayOfWeek: %A" minDate minDate.DayOfWeek;; +DateTime(2025, 12, 31).DayOfYear;; +``` + +All results come back in one read. Send follow-up submissions to the **same session** — previous bindings are still alive. + +### Step 3: End session + +Terminate the process. + +## Example: Exploring System.Console API + +Single `dotnet fsi --nologo` session: + +```fsharp +open System.Reflection;; +let methods = typeof.GetMethods(BindingFlags.Public ||| BindingFlags.Static);; +methods |> Array.map (fun m -> m.Name) |> Array.distinct |> Array.sort;; +``` + +Then drill into a specific method in the same session: + +```fsharp +methods +|> Array.filter (fun m -> m.Name = "Beep") +|> Array.iter (fun m -> + let ps = m.GetParameters() |> Array.map (fun p -> sprintf "%s: %s" p.Name (p.ParameterType.Name)) + printfn "%s(%s)" m.Name (String.concat ", " ps));; +``` +Outputs: +``` +Beep() +Beep(frequency: Int32, duration: Int32) +``` + +## Directives + +| Directive | Purpose | +|---|---| +| `#r "nuget: Pkg";;` | Pull in a NuGet package on the fly | +| `#time "on";;` | Elapsed time, CPU, GC stats per eval | +| `#help List.map;;` | Inline docs for any function | +| `#r "path.dll";;` | Reference local assembly | +| `#load "file.fsx";;` | Load and run script | +| `#quit;;` | Exit session | + +## Pitfalls + +| Pitfall | Fix | +|---------|-----| +| Forgetting `;;` | FSI shows `-` prompt, waiting. Send `;;` alone to flush. | +| New process per submission | **Don't.** Reuse same session — state lost on restart. | +| One expression per submission | A single `;;`-terminated submission can contain multiple `let` bindings, `open` statements, and expressions. Batch them. | diff --git a/tests/dotnet/dotnet-fsi-interactive/eval.yaml b/tests/dotnet/dotnet-fsi-interactive/eval.yaml new file mode 100644 index 0000000000..f971c0e770 --- /dev/null +++ b/tests/dotnet/dotnet-fsi-interactive/eval.yaml @@ -0,0 +1,27 @@ +scenarios: + - name: "Explore System.IO.Path edge cases at runtime" + prompt: > + I need to understand some edge cases in .NET's System.IO.Path API. Can you + verify these at runtime: + (1) What does Path.GetExtension return for ".gitignore" — is it empty or ".gitignore"? + (2) What does Path.Combine("/usr", "/etc/passwd") return — does it concatenate or + does the second absolute path replace the first? + (3) How does Path.Join differ from Path.Combine on the same inputs? + (4) What does Path.GetFileNameWithoutExtension(".gitignore") return? + Don't guess — these have surprising answers. Run .NET code to check. + assertions: + - type: "exit_success" + - type: "output_contains" + value: ".gitignore" + - type: "output_matches" + pattern: "(Combine|Join)" + - type: "output_matches" + pattern: "(/etc/passwd|replace)" + expect_tools: ["bash"] + max_turns: 10 + rubric: + - "The agent correctly reports that Path.GetExtension('.gitignore') returns '.gitignore' (the whole filename is treated as extension)" + - "The agent correctly reports that Path.Combine('/usr', '/etc/passwd') returns '/etc/passwd' — the second absolute path replaces the first" + - "The agent correctly reports that Path.Join('/usr', '/etc/passwd') returns '/usr/etc/passwd' — it concatenates without replacing" + - "The agent verifies the answers by executing .NET code at runtime" + timeout: 120