Research & documents
A research harness reads sources, computes over data and hands back finished work: a report as a PDF, a model as a spreadsheet, a deck. Its weight is in the workspace. Instead of a menu of document tools, the model gets a filesystem, a script runtime and a standard library, and it writes code against your files. Big data stays in files and variables; only the answer reaches the context window. Before it hands anything over, it renders its own output and looks at it.
- The system prompt: what good work looks like for your readers
- Functions over your data sources (registered with defineFn), if the REPL is used
- A vision adapter: one describe({ bytes, mediaType, prompt }) function
- Delivery: what happens when the agent presents a file
How it fits together
Someone uploads a spreadsheet of quarterly sales and asks for a two-page brief with a chart. The host mounts the file into the working environment's /inbox. The model starts from the tree: ls /skills shows worked recipes with the exact import line for each module, and /std documents the adapters you registered.
It writes a script, /scripts/summarise.ts, that imports env:spreadsheets, reads the sheet as plain records and computes the numbers.run_script executes it in a worker thread with no network or host access, and a large result spills to a file rather than flooding the context. A second script builds the brief with env:documents and writes /out/brief.pdf. Then the model calls view_image on page 2, sees that the table runs off the page, fixes the script and runs it again. checkpoint and undo cover anything it wants to roll back. Finally it calls present on the PDF and your host delivers it.
The scripts remain. The next request (“same brief for Q3”) reuses summarise.ts, and in Foundry the whole tree is snapshotted between runs, so the agent's script library becomes accumulated capability rather than lost context.
Skeleton
The core of examples/document-desk, which runs this server-side behind a Next.js app.
import { Displaymanager, Glove, MemoryStore, createAdapter } from "glove-core";
import { createWorkingEnvironment, mountWorkingEnvironment } from "glove-working-environment";
import { documents } from "glove-env-documents";
import { spreadsheets } from "glove-env-spreadsheets";
import { slides } from "glove-env-slides";
import { render } from "glove-env-render";
import { fetchFiles } from "glove-env-fetch";
import { visionAdapter } from "./vision.js"; // your code: { describe({ bytes, mediaType, prompt }) }
import { deliver } from "./deliver.js"; // your code: email it, upload it, stream it to the UI
export async function createAnalyst(sessionId: string) {
const env = await createWorkingEnvironment({
stdlib: [
documents(), // env:documents: PDF and DOCX from one spec; extract text
spreadsheets(), // env:spreadsheets: workbooks as plain records
slides(), // env:slides: decks in and out
render(), // env:render: pages to PNG, so the agent can look at its output
fetchFiles({ allowedDomains: ["data.worldbank.org", "*.ons.gov.uk"] }), // env:fetch, host policy
],
vision: visionAdapter(), // adds view_image
onPresent: ({ name, bytes }) => deliver(sessionId, name, bytes), // adds present
});
const agent = new Glove({
store: new MemoryStore(`analyst:${sessionId}`),
model: createAdapter({ provider: "anthropic", model: "claude-sonnet-5-5" }),
displayManager: new Displaymanager(),
systemPrompt: "You are an analyst. Work in files and scripts. Check every deliverable with view_image before you present it.",
serverMode: true,
compaction_config: {
compaction_instructions:
"Keep the request, the files produced in /out, and what each script in /scripts does.",
},
}).build();
mountWorkingEnvironment(agent, { env }); // the verbs, plus a preamble describing the tree
return { agent, env };
}When the sources are APIs rather than files, add a REPL. Each function you register becomes callable from one execute_js tool, so the model can loop, filter and join in a single program instead of a tool call per row:
import { JsSession, defineFn, mountJs } from "glove-js";
import { z } from "zod";
const session = JsSession.create();
session.register(defineFn({
name: "stats__series",
description: "Monthly values of one indicator for one country.",
input: z.object({ indicator: z.string(), country: z.string() }),
readOnlyHint: true,
handler: (args) => statsApi.series(args as { indicator: string; country: string }), // your client
}));
mountJs(agent, { session }); // execute_js + discovery tools, and a prompt preambleIn Foundry
The same environment becomes an agent field. With a persistence adapter, Foundry restores the tree at the start of every run and snapshots it before cleanup, so /scripts and /out survive between messages and restarts:
import { defineAgent, defineWorkingEnvironment, foundryDataEnvironmentPersistence } from "glove-foundry";
import { documents } from "glove-env-documents";
import { spreadsheets } from "glove-env-spreadsheets";
import { render } from "glove-env-render";
const workbench = defineWorkingEnvironment({
options: { stdlib: [documents(), spreadsheets(), render()], limits: { maxVfsBytes: 256 * 1024 * 1024 } },
persistence: foundryDataEnvironmentPersistence({ scope: "agent" }), // one tree per instance
});
export default defineAgent({
description: "Produces briefs, models and decks as files",
workingEnvironment: workbench,
// model, systemPrompt, store …
});What not to build
- read_pdf, write_docx and make_chart tools. The working environment and its format adapters let the model compose any of those in a script.
- A code runner with filesystem access. Scripts run in terminable worker threads and import only what you inject: no ambient network, host files or processes.
- Copying documents into the prompt. Data stays in the VFS; agents pass paths, not bodies.
- Fifty API tools. A REPL takes the whole catalogue behind one tool and returns only the program's answer.
- Your own check-the-output loop.
render()plus a vision adapter gives the agentview_image, which catches the defects reading the text back cannot.
Deploy on Kuiper
- Deploy the Foundry app as one unit (
kuiper deploy --source . -u analyst --wait) withFileFoundryDataAdapterunder/data: VFS snapshots are stored through the data adapter, so the volume keeps them. Size the volume for your files withresources.volumeGiBinkuiper.json. - PDF and image rendering need nothing installed. Rendering Word and Excel files needs LibreOffice in the image; Kuiper builds from your
Dockerfilewhen the repository has one. - Route the chat and vision models through the gateway (
${{models.BASE_URL}}and${{models.API_KEY}}) so every call is recorded with tokens and cost. - For untrusted code or tools the script runtime does not have (Python, a compiler), give the agent a sandbox as well;
kuiper/documentsis an image with the usual document tooling.
Full walkthrough: Deploy on Kuiper.
Go further
- examples/document-desk and examples/analyst-desk: runnable desks with uploads, a file browser and measured scenarios.
- Build the parts one at a time: give it a workspace, split the work.
- Reference: Working environment, Virtual filesystem, Code execution, Foundry working environments.