From fd5030ff5cd4193b51770f0f4fb7955efb6a7dfa Mon Sep 17 00:00:00 2001 From: Beforerr Date: Fri, 25 Sep 2026 13:02:16 -0600 Subject: [PATCH] feat(julia): eval file runs in fresh module Also invokelatest error rendering to silence 1.12 world-age warnings. --- README.md | 3 ++- go/julia/adapter.go | 4 +++- go/julia/runtime.jl | 2 +- go/julia_integration_test.go | 17 +++++++++-------- skills/repld/SKILL.md | 5 +++-- 5 files changed, 18 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index dbb551f..c06448d 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,8 @@ One Go binary is both the CLI client and the background daemon (auto-started on - This tool is the minimal slice: one dependency-free binary, a plain text protocol — trading rich MIME output for simplicity and zero setup. - Model Context Protocol (MCP) Server is less composable than shell tools and infeasible to use from outside AI sessions. - [mcp-repl](https://github.com/posit-dev/mcp-repl) for persistent Python/R with sandboxing, inline plot images, curated oversized output. - - [julia-mcp](https://github.com/aplavin/julia-mcp?tab=readme-ov-file) for Julia + - [julia-mcp](https://github.com/aplavin/julia-mcp), [AgentREPL.jl](https://github.com/samtalki/AgentREPL.jl) (Malt.jl workers), [Kaimon.jl](https://github.com/kahliburke/Kaimon.jl) (attaches to running Julia processes, debugging/introspection tools), [MCPRepl.jl](https://discourse.julialang.org/t/ann-mcprepl-jl-share-your-repl-with-your-ai-agent/130536) (shares your own REPL) for Julia - Julia + - [DaemonMode.jl](https://github.com/dmolina/DaemonMode.jl) is the closest, but the client is itself a `julia` process and the server is started manually per port. [pi-julia](https://pi.dev/packages/pi-julia) wraps it for the pi agent. - [DaemonicCabal.jl](https://github.com/tecosaur/DaemonicCabal.jl) only runs on Linux - [Malt.jl](https://github.com/JuliaPluto/Malt.jl) manages isolated Julia worker processes _from within Julia_ (used by Pluto). Both run code in persistent, crash-isolated subprocesses, but Malt is a Julia library: its driver must be Julia, and it returns native typed values over Julia's serialization. This tool targets non-Julia callers — a single dependency-free binary speaking a text protocol, so any language/shell/agent can drive it and interpreter versions can be mixed freely. diff --git a/go/julia/adapter.go b/go/julia/adapter.go index 10597ab..6a6467b 100644 --- a/go/julia/adapter.go +++ b/go/julia/adapter.go @@ -61,11 +61,13 @@ func (Adapter) EvalFileStmt(path string, args []string) string { for i, a := range args { argElems[i] = fmt.Sprintf(`String(hex2bytes("%s"))`, hex.EncodeToString([]byte(a))) } + // Fresh module per run: scripts only share loaded packages and compiled code, + // evaluated inside a throwaway parent so no binding accumulates in Main. return fmt.Sprintf(`let _p = String(hex2bytes("%s")), _old = copy(ARGS), _oldpf = Base.PROGRAM_FILE empty!(ARGS); append!(ARGS, AbstractString[%s]) Base.PROGRAM_FILE = _p try - Base.include(Main, _p) + Base.include(Core.eval(Module(), Expr(:module, true, :Script, Expr(:block))), _p) finally empty!(ARGS); append!(ARGS, _old) Base.PROGRAM_FILE = _oldpf diff --git a/go/julia/runtime.jl b/go/julia/runtime.jl index f799cd6..26dc342 100644 --- a/go/julia/runtime.jl +++ b/go/julia/runtime.jl @@ -250,7 +250,7 @@ function run(hex_code, print_result) flush(stdout) _write_control("OK") else - short, smart, full = _render_error(err, bt) + short, smart, full = Base.invokelatest(_render_error, err, bt) flush(stdout) _write_control(string("ERR ", _hex(short), " ", _hex(smart), " ", _hex(full))) end diff --git a/go/julia_integration_test.go b/go/julia_integration_test.go index 060723d..c8192a0 100644 --- a/go/julia_integration_test.go +++ b/go/julia_integration_test.go @@ -401,18 +401,19 @@ func TestJuliaFileEvalArgsAndState(t *testing.T) { cwd := sharedJuliaCwd(t) script := filepath.Join(t.TempDir(), "script.jl") write := func(body string) { require.NoError(t, os.WriteFile(script, []byte(body), 0644)) } - write("println(join(ARGS, \",\"))\nfile_marker = 11\n") + helper := filepath.Join(filepath.Dir(script), "helper.jl") + require.NoError(t, os.WriteFile(helper, []byte("helper_marker = 7\n"), 0644)) + write("println(join(ARGS, \",\"))\nstruct FileS; a::Int; end\nfile_marker = 11\ninclude(\"helper.jl\")\nprintln(helper_marker)\n") res := repldOK(t, socketPath, cwd, "julia", script, "a", "b") - require.Equal(t, "a,b\n", res.stdout) - check := repldOK(t, socketPath, cwd, "julia", "-e", "println(file_marker)") - require.Equal(t, "11\n", check.stdout) - check = repldOK(t, socketPath, cwd, "julia", "-e", "println(length(ARGS))") - require.Equal(t, "0\n", check.stdout) + require.Equal(t, "a,b\n7\n", res.stdout) + check := repldOK(t, socketPath, cwd, "julia", "-e", "println(isdefined(Main, :file_marker), length(ARGS))") + require.Equal(t, "false0\n", check.stdout) - write("println(\"edited\")\n") + // Globals from a previous run are gone; a changed struct redefines cleanly. + write("println(isdefined(@__MODULE__, :file_marker))\nstruct FileS; a::Float64; end\n") res = repldOK(t, socketPath, cwd, "julia", script) - require.Equal(t, "edited\n", res.stdout) + require.Equal(t, "false\n", res.stdout) } // A request queued behind another eval must not interrupt running eval when client disconnects. diff --git a/skills/repld/SKILL.md b/skills/repld/SKILL.md index aa307ef..acdc03e 100644 --- a/skills/repld/SKILL.md +++ b/skills/repld/SKILL.md @@ -1,6 +1,6 @@ --- name: repld -description: "Evaluate Julia/Python/R/Wolfram in long-lived background sessions so imports, variables, and project state persist across calls. Use for iterative work — package development, REPL-style experiments, tests, benchmarks — where starting fresh wastes tokens and time. Use prudently for reproducible research." +description: "Evaluate Julia/Python/R/Wolfram in long-lived background sessions so imports, variables, and project state persist across calls. Use for iterative work — package development, REPL-style experiments, tests, benchmarks — where starting fresh wastes tokens and time. Prefer file-based execution for reproducible research." --- ## Preferred workflow @@ -24,7 +24,8 @@ repld --session scratch julia -E 'x = 1' # named session across directories cd /tmp && repld --session scratch julia -E 'x' # reuse existing named session # File mode: write/update scripts, then eval them in the warm session -repld julia analysis.jl +repld julia analysis.jl # fresh module per run: packages persist, globals don't +repld julia -e 'include("analysis.jl")' # keep globals in Main repld python3 train.py 50 # 50 → sys.argv[1] ```