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

KenmerkTraditionele tool callingCode-mode met monty-go
LLM callsEén per tool-gebruikEén voor alle tools
LatentieN × model round-trip1 × model round-trip + $\mu$s uitvoering
KostenN × input/output tokens1 × input/output tokens
LogicaLLM redeneert stap voor stapLLM schrijft de logica in één keer
Control flowGeen (alleen sequentieel)Loops, conditionele variabelen
FoutafhandelingLLM moet reageren op elke fouttry/except in Python
Security
(tools zijn Go-functies)
(WASM sandbox + callbacks)

Hoe het werkt

Architectuurstroom:

  1. Go Applicatie: Roept runner.Execute(ctx, code, inputs, opts) aan.
  2. wazero: Een pure-Go WASM runtime die de monty.wasm (2,9 MB) binary laadt (via go:embed).
  3. Monty Python Interpreter: Gecompileerd naar wasm32-wasi.
  4. Interactie: Wanneer een externe call plaatsvindt, pauzeert de interpreter.
  5. Callbacks: De aanroep gaat naar de ExternalFunc, OsCallFunc of PrintFunc in de Go-code.
  6. 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 met montygo.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

PythonGo (resultaat)Go (input)
intfloat64int, float64
floatfloat64float64
strstringstring
boolboolbool
Nonenilnil
list, tuple[]any[]any
dictmap[string]anymap[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/while loops, if/elif/else, break/continue.
  • try/except/finally/else, raise en 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() met sep en end kwargs.
  • 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 van sys, typing en asyncio.
  • import os en from pathlib import Path (via OsCallFunc).
  • 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").
  • match statements (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