Code Execution

Loading fifty tool definitions into a context window is expensive and, past a point, counter-productive. The alternative Glove ships is one eval tool: expose the agent's capabilities as functions in a tiny sandboxed interpreter, and let the model discover, call and compose them by writing programs.

Three surfaces, one catalog. glove-js, glove-python and glove-lisp all consume the same ToolFn catalog from glove-scratchpad, so a set of functions mounts on any of them unchanged. Pick the language your models are most fluent in.

PackageToolBet
glove-jsexecute_jsJavaScript — the most-represented language in training data
glove-pythonexecute_pythonPython — what models reach for when the task is data manipulation
glove-lispexecute_lispA Clojure-flavored Lisp — plus the scratchpad's staged-effect contract

Why a REPL beats more tools

The scratchpad work showed that folding capabilities behind ONE code-eval tool beats loading dozens of tool definitions — on correctness, on context, and on cost — because the model computes over results in the sandbox rather than round-tripping every intermediate through its context window. What each surface keeps from that work:

  • One tool, progressive in-band discovery. Nothing is primed by default. The model jumps straight to matches with search("open pull requests"), or browses servers()fns("github") describe("name"). The same tiers also exist as native tools (search_functions, list_servers, list_functions, describe_function) so a weak model can fire them as tool calls and a capable one can script the whole sweep in one program.
  • Off-context data flow. const prs = github.list_pull_requests() stores the rows in the REPL and echoes only a summary; the model then works with prs.length and prs.slice(0, 5).
  • Branch in one program. Decide-and-act is ONE call, not a read, a look, and a second call.
  • Exactly-once effects by construction. A tool call fires when its expression evaluates — there is no planner that might re-run it.
  • Persistent session. Top-level bindings survive across calls, so the model builds up state without re-fetching.
  • Bounded output. What crosses back into context is structurally elided (arrays past 25 items, strings past 300 chars) with a marker naming the true size.

Quick start

terminalbash
pnpm add glove-js glove-scratchpad     # or glove-python / glove-lisp
repl.tstypescript
import { JsSession, mountJs } from "glove-js";
import { fnsFromMcp } from "glove-scratchpad/fns/mcp";

const session = JsSession.create();

// A whole MCP server becomes functions: github__list_pull_requests, …
session.registerAll(await fnsFromMcp(githubConn));

mountJs(agent, { session });   // folds execute_js + discovery tools, primes the prompt

Now the model works entirely in JavaScript through one tool:

what the model writesjavascript
const prs = github.list_pull_requests({ state: "open" });
const stale = prs.filter(p => p.age_days > 30);
stale.length === 0
  ? "all fresh"
  : `${stale.length} stale: ${stale.map(p => p.number).join(", ")}`;

One call. The rows never enter the model's context — only the answer string does.

The function catalog

A capability is a ToolFn: a name, an optional input schema (JSON Schema or Zod), and a call. There are no columns, no pushdown keys and no volatility classes to declare — which is exactly what makes this the right surface when the tools are unknown up front, like an arbitrary MCP server discovered at runtime.

catalog.tstypescript
import { defineFn, fnFromTool } from "glove-scratchpad";
import { fnsFromMcp } from "glove-scratchpad/fns/mcp";
import { z } from "zod";

// A whole MCP server → functions
session.registerAll(await fnsFromMcp(conn));

// An existing Glove tool → a function
session.register(fnFromTool(myTool));

// Or author one inline
session.register(defineFn({
  name: "email__send",
  input: z.object({ to: z.string(), subject: z.string() }),
  readOnlyHint: false,
  handler: (args) => sendEmail(args),
}));

A __ in a name becomes a namespace: github__list_pull_requests binds both the flat name and github.list_pull_requests. Calling an effectful function FIRES it immediately — there is no staging or undo on the JS/Python surfaces; the write verb is the function. (The Lisp surface adds staging — see below.)

The languages

JavaScript

A deliberately small subset — the JavaScript a model reaches for when it thinks “transform this data”, and nothing else: const/let, arrow functions, template literals, destructuring, spread, optional chaining, the usual control flow, the array and string methods, Object.*, Math, JSON, Set/Map/Date/RegExp, and captured console.log. Tool calls are async functions whose promises resolve automatically, so await is optional.

Rejected with a targeted message rather than gibberish: class, import/require, eval, Function, this, prototypes, fetch, for…in, var, in/instanceof, generators.

Python

what the model writespython
prs = github.list_pull_requests(state="open")
stale = [p for p in prs if p["age_days"] > 30]
"all fresh" if len(stale) == 0 else f"{len(stale)} stale"

Lisp

The Lisp surface is built on the scratchpad's ResourceTable contract as well as the function catalog — so it can stage several outbound effects, preview them, and commit or discard as a real dry run:

what the model writesclojure
;; discover
(tables)
(describe :github_pull_requests)

;; keep big intermediates in the REPL, out of context
(def prs (github_pull_requests))     ; echoes {:defined "prs" :count 320}
(frequencies :state prs)

;; BRANCH in one call — decide-and-act
(if (empty? (pagerduty_incidents {:urgency "high" :status "triggered"}))
  (insert! :slack_messages {:channel "ops" :text "All clear."})
  (insert! :emails {:to_addr "oncall@acme.io" :subject "Incidents live"}))

;; stage several effects, preview, then fire — or discard
(stage (insert! :emails {:to_addr "a@b.io" :subject "one"})
       (insert! :emails {:to_addr "c@d.io" :subject "two"}))
(commit!)   ; or (rollback!)

How a program runs

parse → validate → run. The parser builds the full program; a whitelist walk rejects unsupported constructs before anything executes. Then an async tree-walking evaluator runs it with a fuel budget (per node and per loop back-edge, so while (true) {} cannot hang), a recursion-depth cap, and AbortSignal support.

Every member read and method call goes through a sandbox boundary that blocks the escape keys (constructor, __proto__, prototype, call/apply/bind) and exposes a fixed allowlist — a program cannot climb a constructor chain back to the host.

Framing: repl, program, or workflow

The eval tool ships three interchangeable framings, chosen at mount time. The runtime is identical — only the tool name and the primed preamble change:

repl.tstypescript
mountJs(agent, { session });                     // execute_js          (default)
mountJs(agent, { session, frame: "program" });   // execute_js_program
mountJs(agent, { session, frame: "workflow" });  // execute_js_workflow

The bet: the token “REPL” pattern-matches to an interactive, line-by-line session, so models degrade the surface back into an incremental tool-call loop — peek at a row, then run a second program. The workflow framing never says REPL; it frames the call as ONE complete program carrying the task start to finish, and demotes cross-call persistence to a retry-only recovery aid. program is the half-step, so a benchmark can separate the name alone from the full reframing.

Discovery modes

Result shapes warm lazily — a function's row type is sampled the first time it is described, not for the whole catalog at mount, and surfaces as a TS-like type in describe(...):

describe outputtext
sentry__list_issues(…) → { …, count: number,
                            status: "unresolved"|"resolved"|"ignored" }[]

discovery: "full" primes every signature up front for small catalogs; "auto" picks per size. Result-shape discovery is what closed the weak-model gap in the live A/B — the JavaScript arm moved 78% → 90% → 97% over two hardening batches, above Lisp (95%) and SQL (92%), for a modest peak-context increase.

  • Scratchpad — the SQL surface over the same idea, with typed resources and staged writes
  • Egress Control — make this boundary a measured, enforced privacy boundary
  • Working Environment — when the agent needs state that accumulates across calls
  • MCP — where a runtime-discovered catalog usually comes from