monty-go: Veilig LLM-gegenereerde Python uitvoeren vanuit Go
Een Go-agent schrijft Python-code, monty-go voert deze uit in een gesandboxte WASM-instantie met een opstarttijd van minder dan een milliseconde, en pauzeert telkens wanneer de code een externe functie aanroept, zodat de Go-code dit kan afhandelen.
Installatie:
go get github.com/fugue-labs/monty-go
Waarom monty-go?
LLM's werken sneller, goedkoper en betrouwbaarder wanneer ze code schrijven in plaats van sequentiële tool-calls te maken.
Traditionele aanpak:
- Agent →
tool_call("search", {query: "weather london"})→ resultaat - Agent →
tool_call("search", {query: "weather tokyo"})→ resultaat - Agent →
tool_call("compare", {a: result1, b: result2})→ resultaat
Aanpak met monty-go: De LLM schrijft één script:
london = search(query="weather london")
tokyo = search(query="weather tokyo")
compare(a=london, b=tokyo)
Dit reduceert drie model-calls naar één call. De Python-code roept Go-functies aan, Monty pauzeert bij elke aanroep, de Go-code voert de actie uit en Monty gaat daarna verder. Dit alles gebeurt via een WASM-binair bestand van 2,9 MB dat is ingebed in je Go-binary.
Voor verdere motivatie kan worden gekeken naar:
- Programmatic Tool Calling (Anthropic)
- Code Execution with MCP (Anthropic)
- Code Mode (Cloudflare)
- Smol Agents (Hugging Face)
Snelstart
package main
import (
"context"
"fmt"
"log"
montygo "github.com/fugue-labs/monty-go"
)
func main() {
runner, err := montygo.New()
if err != nil {
log.Fatal(err)
}
defer runner.Close()
result, err := runner.Execute(context.Background(),
"x * 2 + y",
map[string]any{"x": 10, "y": 5},
)
if err != nil {
log.Fatal(err)
}
fmt.Println(result) // 25
}
Externe Functies (Pauzeren/Hervatten)
De werkelijke kracht ligt in externe functieaanroepen. Monty pauzeert de uitvoering zodra de Python-code een functie aanroept die jij hebt gedeclareerd. De Go-callback handelt dit af en Monty hervat de uitvoering met de retourwaarde:
result, err := runner.Execute(ctx,
`
london = get_weather("London")
tokyo = get_weather("Tokyo")
f"{london['city']}: {london['temp']}°C, {tokyo['city']}: {tokyo['temp']}°C"
`,
nil,
montygo.WithExternalFunc(func(ctx context.Context, call *montygo.FunctionCall) (any, error) {
city, _ := call.Args["city"].(string)
// Implementatie hier — bijv. HTTP call, database query, etc.
return map[string]any{"city": city, "temp": 22}, nil
}, montygo.Func("get_weather", "city")),
)
// result: "London: 22°C, Tokyo: 22°C"
Voor meerdere functies kun je deze allemaal registreren en dispatcheren op basis van de naam:
result, err := runner.Execute(ctx, code, nil,
montygo.WithExternalFunc(func(ctx context.Context, call *montygo.FunctionCall) (any, error) {
switch call.Name {
case "search":
return doSearch(call.Args)
case "calculate":
return doCalculate(call.Args)
case "store":
return doStore(call.Args)
default:
return nil, fmt.Errorf("unknown function: %s", call.Name)
}
},
montygo.Func("search", "query"),
montygo.Func("calculate", "expression"),
montygo.Func("store", "key", "value"),
),
)
Resource-limieten
Om oneindige loops of excessief geheugengebruik te voorkomen, kunnen limieten worden ingesteld:
result, err := runner.Execute(ctx, code, inputs,
montygo.WithLimits(montygo.Limits{
MaxDuration: 5 * time.Second,
MaxMemoryBytes: 10 * 1024 * 1024, // 10 MB
MaxAllocations: 100000,
MaxRecursionDepth: 100,
}),
)
Oneindige loops, "memory bombs" en te diepe recursie worden netjes beëindigd met een *MontyError. Ook deadlines van Go's context.Context worden gerespecteerd; als de context wordt geannuleerd, stopt de WASM-instantie.
Print Capture
Je kunt de output van Python's print() functie vastleggen:
var output strings.Builder
_, err := runner.Execute(ctx, `print("step 1 done")`, nil,
montygo.WithPrintFunc(func(s string) { output.WriteString(s) }),
)
fmt.Print(output.String()) // "step 1 done\n"
OS-aanroepen
Toegang tot het bestandssysteem en de omgeving verloopt via een Go-callback:
result, err := runner.Execute(ctx,
`
from pathlib import Path
data = Path("/config/settings.json").read_text()
data
`,
nil,
montygo.WithOsCallFunc(func(ctx context.Context, call *montygo.OsCall) (any, error) {
switch call.Function {
case "Path.read_text":
path, _ := call.Args[0].(string)
return readFromYourStorage(path)
case "Path.exists":
path, _ := call.Args[0].(string)
return existsInYourStorage(path), nil
default:
return nil, fmt.Errorf("blocked: %s", call.Function)
}
}),
)
Er vindt geen bestandssysteemtoegang plaats tenzij jouw callback dit expliciet toestaat.
Integratie met Gollem
monty-go is ontworpen om "code-mode" te voeden in Gollem, het productie-agent framework voor Go. In plaats van sequentiële tool-calls schrijft de LLM Python-code die jouw tools als functies aanroept.
Voorbeeld in Gollem:
import (
"github.com/fugue-labs/gollem"
"github.com/fugue-labs/gollem/provider/anthropic"
montygo "github.com/fugue-labs/monty-go"
)
// Bestaande Gollem tools
searchTool := gollem.FuncTool[SearchParams]("search", "Search the knowledge base", doSearch)
calcTool := gollem.FuncTool[CalcParams]("calculate", "Run calculations", doCalc)
// Maak een code-mode tool die de toolset wrapt met Monty
codeMode := NewCodeModeTool(runner, searchTool, calcTool)
agent := gollem.NewAgent[Analysis](anthropic.New(),
gollem.WithTools[Analysis](codeMode),
gollem.WithSystemPrompt[Analysis](`You have a code execution tool.
Write Python code to call the available functions. Available functions:
- search(query: str) -> dict: Search the knowledge base
- calculate(expression: str) -> float: Evaluate math expressions
Write code that calls these functions and returns the result.`),
)
result, _ := agent.Run(ctx, "Compare Q3 and Q4 revenue and calculate the growth rate")
Met één model-call schrijft de LLM:
q3 = search(query="Q3 revenue")
q4 = search(query="Q4 revenue")
growth = calculate(expression=f"({q4['revenue']} - {q3['revenue']}) / {q3['revenue']} * 100")
{"q3": q3, "q4": q4, "growth_rate": growth}
Vergelijking: Traditionele Tool Calling vs. Code-mode met monty-go
| Kenmerk | Traditionele tool calling | Code-mode met monty-go |
|---|---|---|
| LLM calls | Eén per tool-gebruik | Eén voor alle tools |
| Latentie | N × model round-trip | 1 × model round-trip + $\mu$s uitvoering |
| Kosten | N × input/output tokens | 1 × input/output tokens |
| Logica | LLM redeneert stap voor stap | LLM schrijft de logica in één keer |
| Control flow | Geen (alleen sequentieel) | Loops, conditionele variabelen |
| Foutafhandeling | LLM moet reageren op elke fout | try/except in Python |
| Security | � | |
| (tools zijn Go-functies) | � | |
| (WASM sandbox + callbacks) |
Hoe het werkt
Architectuurstroom:
- Go Applicatie: Roept
runner.Execute(ctx, code, inputs, opts)aan. - wazero: Een pure-Go WASM runtime die de
monty.wasm(2,9 MB) binary laadt (viago:embed). - Monty Python Interpreter: Gecompileerd naar
wasm32-wasi. - Interactie: Wanneer een externe call plaatsvindt, pauzeert de interpreter.
- Callbacks: De aanroep gaat naar de
ExternalFunc,OsCallFuncofPrintFuncin de Go-code. - Hervatten: De Go-code stuurt het resultaat terug en de interpreter hervat de uitvoering.
Belangrijke technische details:
- Geen CGO: wazero is een pure-Go runtime.
- Geen subprocessen: De WASM binary is ingebed en wordt bij opstarten één keer gecompileerd.
- Isolatie: Elke
Execute()call krijgt een verse, geïsoleerde WASM-instantie. Geen state-leaks tussen calls. - JSON-boundary: Gegevens tussen Go en WASM worden uitgewisseld via JSON.
API-overzicht
// Maak een herbruikbare runner. Compileert de WASM module één keer.
runner, err := montygo.New()
defer runner.Close()
// Voer Python code uit met inputs en opties.
result, err := runner.Execute(ctx, code, inputs, opts...)
Beschikbare opties:
montygo.WithExternalFunc(fn, ...): Registreer aanroepbare functies metmontygo.Func("naam", "parameter").montygo.WithOsCallFunc(fn): Beheer toegang tot bestandssysteem/omgeving.montygo.WithLimits(montygo.Limits{...}): Stel resource-limieten in.montygo.WithPrintFunc(fn): Leg print-output vast.
Type-mapping
| Python | Go (resultaat) | Go (input) |
|---|---|---|
int | float64 | int, float64 |
float | float64 | float64 |
str | string | string |
bool | bool | bool |
None | nil | nil |
list, tuple | []any | []any |
dict | map[string]any | map[string]any |
set | []any | — |
Foutafhandeling
Python-exceptions worden omgezet naar *montygo.MontyError:
result, err := runner.Execute(ctx, "1 / 0", nil)
var me *montygo.MontyError
if errors.As(err, &me) {
fmt.Println(me.Message) // "Traceback... ZeroDivisionError: division by zero"
}
Functionaliteiten van Monty
Wat Monty KAN (gebaseerd op v0.0.11):
- Rekenkunde, string-operaties, f-strings en slicing.
- Functies, lambdas, closures en generators.
for/whileloops,if/elif/else,break/continue.try/except/finally/else,raiseen de exception-hiërarchie.- List/dict/set comprehensions.
- Built-ins:
range,len,sum,min,max,sorted,reversed,enumerate,zip,map,filter,all,any,getattr. - Type-checks:
isinstance,type,int(),float(),str(),bool(),abs(). print()metsepenendkwargs.- PEP 448 generalized unpacking (
*args,**kwargs). - Genest en augmented subscript assignment (
a[i][j] = v,a[i] += 1). - Tuple vergelijkingen (
<,>,<=,>=). - Multi-module imports (
import a, b, c). - Standaardbibliotheek:
math(volledig),re,datetime,json, en subsets vansys,typingenasyncio. import osenfrom pathlib import Path(viaOsCallFunc).- Dataclass-instanties via externe functieaanroepen.
- Resource-limieten (tijd, geheugen, allocaties, recursie-diepte).
Wat Monty NIET KAN:
- Class-definities (alleen dataclass-instanties via externe I/O; class-def is "coming soon").
matchstatements (coming soon).- Context managers (
with ...). - De rest van de standaardbibliotheek en alle third-party libraries.
float('inf')/float('nan')(beperking door JSON-serialisatie).
Tests en Build
Er zijn 97 end-to-end tests beschikbaar die scenario's dekken zoals basisexpressies, exception-types, data type round-tripping, externe functies, resource-limieten, OS-calls en isolatie.
Tests uitvoeren:
make test
Bouwen vanuit broncode: Vereist Rust met wasm32-wasip1 target en Go 1.23+:
rustup target add wasm32-wasip1
make build # compileert Rust $\rightarrow$ WASM
make test # bouwt en draait Go tests
Erkenningen
monty-go is mogelijk gemaakt door Monty, gecreëerd door Samuel Colvin en het Pydantic-team. Monty is een minimalistische, veilige Python-interpreter geschreven in Rust, specifiek gebouwd voor AI-agents. Het inzicht dat LLM's code zouden moeten schrijven in plaats van sequentiële tool-calls te maken, en dat daarvoor een veilige interpreter (geen container) nodig is, maakt code-mode mogelijk.
Licentie
MIT
Groetjes,