Give it a workspace
Past a handful of tools, adding another tool for every operation stops working. The model has to read every definition on every call, and each step of a multi-step job costs a round-trip. A workspace gives the agent somewhere to keep files and run code instead. It writes one program that reads, loops and decides, and only the answer comes back to the conversation.
Pick the surface by the work
| The work looks like… | Give it | Package |
|---|---|---|
| A few well-defined actions | Plain tools | glove-core |
| Many operations, chained with logic in between | One code tool over a function catalogue | glove-js · glove-python · glove-lisp |
| Lots of read-only data to filter and join | Capabilities as SQL tables | glove-scratchpad |
| Building files over many steps: drafts, data, documents | A working environment: files, scripts, checkpoints | glove-working-environment |
| Third-party services that already speak MCP | Mounted MCP servers | glove-mcp |
| The real web, or real code in a real OS | A browser or a sandbox | glove-execution |
These combine. The release assistant uses three of them.
One tool instead of fifty: a REPL
import { JsSession, defineFn, mountJs } from "glove-js";
import { z } from "zod";
const session = JsSession.create();
session.register(defineFn({
name: "git__merged",
description: "Pull requests merged since a date.",
input: z.object({ since: z.string() }),
readOnlyHint: true,
handler: ({ since }) => git.merged(String(since)), // your Git client
}));
session.register(defineFn({
name: "git__diffstat",
description: "Files and line counts changed by a pull request.",
input: z.object({ number: z.number() }),
readOnlyHint: true,
handler: ({ number }) => git.diffstat(Number(number)),
}));
mountJs(agent, { session });The agent now has execute_js plus discovery: it can search the catalogue and describe a function before calling it. To size a release it writes one program (fetch the merged PRs, get each diffstat, total the lines, keep the five largest) and gets back five rows instead of fifty tool results. Bindings persist for the session, so the next program can use what the last one computed. The interpreter has no network, filesystem or process access beyond the functions you registered. Python and Lisp work the same way.
Files that persist: the working environment
import { createWorkingEnvironment, mountWorkingEnvironment } from "glove-working-environment";
import { documents } from "glove-env-documents";
const env = await createWorkingEnvironment({ stdlib: [documents()] });
await env.mount({ text: changelogTemplate }, "/inbox/changelog-template.md");
mountWorkingEnvironment(agent, { env });
// … after the run, collect what it produced:
const files = await env.export("/out/**");The agent gets a closed set of verbs over a virtual filesystem: write_file, edit_file, read_file, ls, grep, run_script, checkpoint, undo and more. Scripts are named files that run in worker threads and can import only what you inject (here env:documents, so it can write the notes as a PDF or DOCX). Big data stays in files; only what the agent reads enters context. Other adapters add spreadsheets, slides, images, media, archives, HTTP with a domain policy, and a renderer so the agent can look at its own output. The tree is a glove-vfs filesystem: back it with a directory or object storage to make it outlive the process.
Services that speak MCP
mountMcp(agent, { adapter, entries }) takes a catalogue of servers your app supports and an adapter that remembers which are active and resolves their tokens. The agent gets each active server's tools, plus a discovery subagent that can find and switch on another server mid-conversation. Your adapter keeps the tokens; the model never sees them. See MCP.
The real web and real machines
mountBrowser(agent, { adapter }) and mountSandbox(agent, { adapter }) from glove-execution add execute_browser and execute_sandbox: the same script-the-whole-step idea, over a browser session or a persistent container. Screenshots reach the model as images on its next call. Station is the first backend; in Foundry, one managed Station can host both. On Kuiper, use Station's containers on the unit, or call Kuiper's browsers and sandboxes through @kuiper-sh/sdk from your own tools once the unit references ${{browsers.…}} or ${{sandboxes.…}}; there is no glove-execution adapter for Kuiper's gateway yet. See browsers and sandboxes and the coding agent blueprint.
Already built for you
| Primitive | What you get | Instead of building |
|---|---|---|
| Virtual filesystemglove-vfs | One tree shared by the working environment, memory resources and REPLs, with path-scoped access rules.mountFs([...]) · withAccess(fs, policy) · inMemoryFs() · hostDirectory(dir) · cachedRemote(store) | letting tools read and write the host filesystem |
| Working environmentglove-working-environment | Files, named scripts in worker threads, checkpoints, undo, run history and artifact export, with no ambient network or host access.mountWorkingEnvironment(glove, { env: await createWorkingEnvironment({ stdlib }) }) | a read/write/run tool set and a code runner |
| Format adaptersglove-env-* | PDF, DOCX, XLSX, PPTX, images, audio, video, archives, HTTP and secrets as script imports, plus rendering pages so the agent can look at its output.stdlib: [documents(), spreadsheets(), slides(), images(), media(), render(), fetchFiles()] | wiring pdf, docx and spreadsheet libraries into tools |
| Browsersglove-execution | execute_browser: the model scripts observe, act and verify in one call, and screenshots arrive as images.mountBrowser(glove, { adapter }) | Playwright wrapped in click/type/screenshot tools |
| Sandboxesglove-execution | execute_sandbox over persistent containers: files, commands, terminals and long-running services, with ownership and cleanup.mountSandbox(glove, { adapter }) | docker exec wrappers and container bookkeeping |
Next
One agent is now doing everything: reading code, drafting, checking and publishing. In step 6 we split it: a reviewer subagent with read-only tools checks the notes before anything ships.