# Glove — condensed reference for language models
Glove is an open-source TypeScript framework for building applications driven by
an AI agent. You define capabilities as **tools**; the agent decides which to
call and in what order. Docs: https://glove.dterminal.net. Source:
https://github.com/porkytheblack/glove.
This file is the whole framework in one place. Everything is TypeScript + ESM.
Tool input schemas are Zod schemas.
## 1. Mental model
- **Tool** — a name, a description (this is what the model reads), a Zod
`inputSchema`, and an async `do(input, display)`. Registered with `.fold()`.
- **Agent loop** — prompt the model, run the tools it asks for, feed results
back, repeat until it answers with text.
- **Display stack** — tools push UI mid-run. `display.pushAndForget()` renders
and keeps going; `display.pushAndWait()` renders and suspends the tool until
the user responds, then resumes with their value.
- **Store** — where conversation state lives (`StoreAdapter`).
- **Adapters** — the seam at every layer: model, store, display, subscriber,
voice. Swap the implementation, keep the app.
## 2. Install
```bash
# Full-stack (Next.js App Router)
pnpm add glove-react glove-next zod
# Server-only (Node / CLI / worker)
pnpm add glove-core zod
```
`glove-core` is a dependency of `glove-react`.
## 3. Server-side agent (glove-core)
```ts
import { Glove, MemoryStore, Displaymanager, createAdapter } from "glove-core";
import { z } from "zod";
const agent = new Glove({
store: new MemoryStore("session-1"),
model: createAdapter({ provider: "anthropic", model: "claude-sonnet-4-20250514", stream: true }),
displayManager: new Displaymanager(),
systemPrompt: "You are a helpful assistant.",
compaction_config: { compaction_instructions: "Summarize the conversation so far." },
})
.fold({
name: "get_weather",
description: "Get current weather for a city",
inputSchema: z.object({ city: z.string() }),
async do(input) {
return { status: "success", data: await weather.lookup(input.city) };
},
})
.build();
const result = await agent.processRequest("What's the weather in Tokyo?");
```
Observe it with a subscriber:
```ts
agent.addSubscriber({
async record(event, data) {
if (event === "text_delta") process.stdout.write(data.text);
if (event === "tool_use") console.log("→", data.name);
if (event === "tool_use_result") console.log("←", data.result.status);
},
});
```
## 4. Full-stack (glove-next + glove-react)
```ts
// app/api/chat/route.ts
import { createChatHandler } from "glove-next";
export const POST = createChatHandler({
provider: "anthropic",
model: "claude-sonnet-4-20250514",
cache: true,
});
```
```tsx
// lib/glove.ts
import { GloveClient } from "glove-react";
import { z } from "zod";
export const gloveClient = new GloveClient({
endpoint: "/api/chat",
systemPrompt: "You are a helpful weather assistant.",
tools: [{
name: "get_weather",
description: "Get the current weather for a city.",
inputSchema: z.object({ city: z.string() }),
async do(input) { return await lookup(input.city); },
}],
});
// app/providers.tsx ("use client")
import { GloveProvider } from "glove-react";
export function Providers({ children }) {
return {children};
}
// app/page.tsx ("use client")
import { useGlove, Render } from "glove-react";
export default function Chat() {
const glove = useGlove(); // { timeline, streamingText, busy, sendMessage, stats, runnable }
return {entry.text}
} />;
}
```
Timeline entries have `kind`: `"user"` | `"agent_text"` | `"tool"`.
## 5. Display stack
```ts
async do(input, display) {
// Show UI, keep running.
await display.pushAndForget({ renderer: "product_grid", input: results });
// Show UI and SUSPEND until the user submits.
const payment = await display.pushAndWait({ renderer: "payment_form", input: cart });
return await orders.create(cart, payment);
}
```
## 6. Model providers
`createAdapter({ provider, model, stream })` from `glove-core/models/providers`.
| provider | env var | default model |
| --- | --- | --- |
| openai | OPENAI_API_KEY | gpt-4.1 |
| anthropic | ANTHROPIC_API_KEY | claude-sonnet-4-20250514 |
| openrouter | OPENROUTER_API_KEY | anthropic/claude-sonnet-4 |
| gemini | GEMINI_API_KEY | gemini-2.5-flash |
| minimax | MINIMAX_API_KEY | MiniMax-M2.5 |
| kimi | MOONSHOT_API_KEY | kimi-k2.5 |
| glm | ZHIPUAI_API_KEY | glm-4-plus |
| mimo | MIMO_API_KEY | mimo-v2.5 |
| bedrock | AWS_ACCESS_KEY_ID | anthropic.claude-3-5-sonnet-20241022-v2:0 |
| ollama | (none) | user-specified |
| lmstudio | (none) | user-specified |
Options: `reasoning: true | { effort, reasoningObject, extraBody, includeInText }`
captures provider reasoning traces; `cache: true | { ttl: "5m" | "1h" }` enables
provider prompt caching. Cache usage is reported on every response as
`cache_creation_input_tokens` / `cache_read_input_tokens`.
## 7. Package map
| package | purpose |
| --- | --- |
| glove-core | runtime: agent loop, tools, models, display manager, stores, hooks/skills/subagents |
| glove-react | GloveClient, GloveProvider, useGlove, , defineTool, createRemoteStore |
| glove-next | createChatHandler — SSE streaming route handler |
| glove-voice | cascade voice: VAD → STT → agent → TTS, barge-in, push-to-talk |
| glove-voice-native | React Native / Expo mic capture, PCM playback, Silero VAD |
| glove-voice-s2s | run an agent on realtime speech-to-speech models (OpenAI Realtime, Gemini Live) |
| glove-voice-avatar | live avatars over the S2S audio (Tavus echo, Anam passthrough) |
| glove-voice-livekit | LiveKit room transport + LiveKit-native avatars |
| glove-memory | entity graph, episodic timeline, resource filesystem, standing context, forms |
| glove-scratchpad | expose tools as a relational database driven by one execute_sql tool |
| glove-sql | zero-dependency Postgres-subset SQL engine (scratchpad's default backend) |
| glove-working-environment | persistent sandboxed VFS: scripts, runs, artifacts |
| glove-env-documents/-spreadsheets/-images/-slides/-archives/-media/-render/-motion | stdlib adapters for the working environment |
| glove-js / glove-python / glove-lisp | one eval tool over a shared ToolFn catalog |
| glove-egress | measured, enforced egress boundary over that catalog |
| glove-image | agentic image generation: prompt pipeline, characters/scenes, refs, edit, assemble, cost |
| glove-mesh | direct/broadcast/ack messaging between agents |
| glove-continuum-signal | subprocess runtime: triggered (cold) and concurrent (warm) agents |
| glove-mcp | bridge Model Context Protocol servers in as tools |
| glovebox-core / -kit / -client | package an agent as a sandboxed container service |
| glove-sqlite | DEPRECATED SQLite store; bring your own StoreAdapter instead |
## 8. Capability packages — minimal correct usage
### glove-memory
```ts
import {
useMemoryReader, useEpisodicReader, useContext,
InMemoryEntityAdapter, InMemoryEpisodicAdapter, InMemoryContextAdapter,
} from "glove-memory";
useMemoryReader(agent, new InMemoryEntityAdapter({ schema: ontology }));
useEpisodicReader(agent, new InMemoryEpisodicAdapter());
useContext(agent, new InMemoryContextAdapter()); // injected into the prompt each turn
// Writes go to a SEPARATE curator instance: useMemoryCurator(curator, entities)
```
Recommended shape: do not attach every memory tool to the main agent. Build one
subagent per retrieval task with `defineSubAgent`, attaching only the adapter
slice it needs, so token cost scales with role rather than ontology size. The
exception is `useContext` — keep that on the agent the user actually talks to.
Memory arrives in strata: a shared corpus the agent reads but must never change
(authored elsewhere, one copy for everyone) plus its own private store. They are
different adapters; `layerEntity` / `layerEpisodic` / `layerResources` /
`layerContext` merge a stack into one adapter of the ordinary contract, so the
usual `use*` helpers fold the usual tools over it and the agent never learns
there are two stores. Exactly one `access: "write"` stratum per stack; reads
merge in layer order; writes route to the owning stratum and are refused when it
is read-only. Entity is the lossy one — edges cannot straddle strata, so model
cross-stratum associations as episode participants or resource links.
Narrow what the agent may do two independent ways, meant to be combined.
`{ tools: { allow, deny } }` on any `use*` helper picks which tools are folded,
so the affordance never reaches the model. `withResourceAccess(adapter, policy)`
gates the resource filesystem by path — `"write"`, `"read"` (mutations refused),
`"none"` (invisible, filtered out of listings and search) over prefix or glob
rules that cascade last-match-wins — so a write into a read-only folder is
refused whichever tool asks.
### glove-memory forms — structured collection over a conversation
The fifth subsystem. Definitions are CODE (zod schemas, gate closures and
executors in one builder chain); the agent never reads them, only a projection
of evaluated state.
```ts
import { z } from "zod";
import { defineForm } from "glove-memory/forms";
export const travelClaim = defineForm({
id: "travel-claim", version: 1,
name: "Travel reimbursement claim",
description: "Claimant, trip, travel and approval details.",
conduct: "Conversational — one or two questions at a time.",
})
.step("claimant", { title: "Claimant", preview: "name, staff id, email" }, (s) =>
s.field("fullName", { schema: z.string().min(2), label: "Full name" })
.field("email", { schema: z.string().email(), label: "Work email" }),
)
.step("travel", { title: "Travel", preview: "mileage or ticket",
when: (v, s) => s.stepComplete("claimant") }, (s) =>
s.field("mode", { schema: z.enum(["car", "rail", "air"]), label: "Mode" })
.field("mileage", { schema: z.number().int().min(1).optional(),
label: "Miles driven",
when: (v) => v.mode === "car" }),
)
.checkpoint("policy-cap", {
when: (v) => typeof v.total === "number" && v.total > 750,
blocking: true,
run: () => ({ fail: "Over the limit — needs Finance pre-approval." }),
})
.onComplete(async (ctx) => { await ctx.memory.upsertNode("Person", { name: ctx.values.fullName }); })
.build();
```
Wiring:
```ts
import { FormRegistry } from "glove-memory/forms";
import { useFormRunner, useFormReader, InMemoryFormAdapter } from "glove-memory";
const registry = new FormRegistry().register("travel-claim", {
name: "…", description: "…",
load: () => import("./forms/travel-claim").then((m) => m.travelClaim), // lazy
});
const { runner } = useFormRunner(glove, new InMemoryFormAdapter({ schema }), {
registry, subject: conversationId,
memory: { entity, episodic, resources, context }, // optional bridge
});
useFormReader(auditor, adapter, { registry }); // read-only history
```
Rules that decide whether generated code is correct:
- **There is no `required` flag.** A field is optional iff its zod schema
accepts `undefined`. The agent-readable `type` string is derived via
`z.toJSONSchema`. Do not invent a field-type vocabulary.
- **Writes are never gated.** `glove_form_fill` accepts a patch of ANY field
ids at any time; only zod can reject. Ids resolve case/punctuation-insensitively
(`full_name` = `Full name` = `fullName`), with `did_you_mean` on a miss.
- **`when` is applicability, steps are ask order.** An inapplicable field is
not asked and does not count toward completion, but a value given for it is
kept as `held` and goes live again if the branch flips back. `values` =
live entries; `held` = the rest; `onComplete` only ever sees `values`.
- **`entries` is an append-only log per field plus a cursor.** Corrections
append. `retract` / `undo` / `redo` are cursor moves, reached by the model
through `glove_form_revise`'s `action` param, not separate tools.
- **Executors** — `field.onFill`, `step.onComplete`, `checkpoint.run`,
`form.onComplete` — fire on RISING EDGES only, in that order, commit-then-run,
at-least-once with a per-occurrence `idempotencyKey`. They return
`{ patch } | { fail } | { jump } | { complete } | { terminate }`, or an array.
A throwing executor does NOT roll back the write; a recorded failure is not
retried.
- **Tiers.** Tier 0 is one system-prompt line per turn (open step + pending
labels + later-step previews). Tier 1 = `glove_form_status` (open step in
full). Tier 2 = `glove_form_inspect` (any step / field / outline).
- **Tools:** `glove_form_list` / `_start` / `_status` / `_inspect` / `_fill` /
`_revise` / `_abandon` from the runner, `glove_form_history` from the reader.
- **A blocking checkpoint** leaves the instance `awaiting` with writes refused
(`form_blocked`) until the host calls `runner.resolveCheckpoint`. No timeout.
- **`FormAdapter`** is storage only. Invariants: entries append never replace
(`applyEntryCommit` is exported), `version` is compare-and-set (throw
`FormConflictError`), a commit is all-or-nothing, reads return snapshots.
- Instances pin `defVersion`; drift defaults to `status: "stale"` unless the
def supplies `migrate(old, fromVersion)`.
### glove-scratchpad (+ glove-sql)
```ts
import { Database, defineResource, mountDatabase } from "glove-scratchpad";
import { z } from "zod";
const db = await Database.create({ policy: { writes: true } });
db.register(defineResource({
name: "github_pull_requests",
volatility: "volatile",
schema: z.object({ number: z.number().int(), title: z.string(), state: z.string() }),
keys: ["number"],
select: (b) => github.listPRs({ state: b.one("state") }), // WHERE pushes down
}));
mountDatabase(agent, { db }); // folds execute_sql + explain_sql and primes the prompt
```
The model then discovers via `information_schema`, invokes by querying a table,
composes with JOINs, acts with INSERT/UPDATE/DELETE, and can stage several
writes inside BEGIN … COMMIT/ROLLBACK as a real dry run.
`glove-sql` standalone:
```ts
import { MemoryBackend } from "glove-sql";
const be = await MemoryBackend.create();
await be.exec("CREATE TABLE orders (id int, total numeric, region text)");
const { rows } = await be.query("SELECT region, sum(total) FROM orders GROUP BY region");
```
### glove-js / glove-python / glove-lisp
```ts
import { JsSession, mountJs } from "glove-js";
import { fnsFromMcp } from "glove-scratchpad/fns/mcp";
const session = JsSession.create();
session.registerAll(await fnsFromMcp(githubConn)); // github__list_pull_requests, …
mountJs(agent, { session }); // execute_js + discovery tools
// mountJs(agent, { session, frame: "workflow" }) → execute_js_workflow framing
```
Same for `PySession`/`mountPy` (`execute_python`) and the Lisp surface
(`execute_lisp`). Author functions with `defineFn` / `fnFromTool` / `fnsFromMcp`
from `glove-scratchpad`. A `__` in a name is a namespace:
`github__list_pull_requests` also binds as `github.list_pull_requests`. Calling an
effectful function fires it immediately — there is no staging on the JS/Python
surfaces.
### glove-working-environment
```ts
import { createWorkingEnvironment, mountWorkingEnvironment, hostDirectory } from "glove-working-environment";
import { documents } from "glove-env-documents";
const env = await createWorkingEnvironment({ stdlib: [documents()], limits: { runTimeoutMs: 30_000 } });
await env.mount("./q3.xlsx", "/inbox/q3.xlsx");
mountWorkingEnvironment(agent, { env });
const out = await env.export("/out/**"); // [{ path, bytes }]
const snap = await env.snapshot(); // checkpoint; restore with fromSnapshot(snap)
```
Tree: `/inbox` inputs, `/scripts` the agent's script library (+ generated .d.ts),
`/skills` worked recipes, `/std` adapter types and docs, `/tmp` intermediates,
`/out` deliverables, `/.env` history. Every script under `/scripts` MUST
default-export a function; validation happens at write time. Scripts may import
relative VFS paths and `env:*` modules only — no network, no host fs, no process.
Backing the tree: `inMemoryFs()` (default), `hostDirectory(dir)` (copy-on-write
over a real directory; `commit()` / `discard()`), `fromSnapshot(snap)`, or
`cachedRemote(store)` for object storage.
Exposing a library to the model — the shape picks the route, and the wrong route
fails quietly:
| library shape | route | call style |
| --- | --- | --- |
| does I/O — reads/writes files, calls out | `defineAdapter` | async |
| stateful builder — `new X()`, chaining, terminal save | `defineBuilder` / `defineBuilders` | async |
| pure computation — no I/O, no state | `definePureModule` | synchronous |
| a capability, not a library — MCP server, model, HTTP API | `defineTools` | async |
`definePureModule({ name, from, description, pick })` imports the package inside
the worker and binds it directly, so calls stay synchronous. `pick` is the
sandbox boundary, not a convenience — never pick a string-to-code member (e.g.
`_.template`, which compiles with `Function(source)`).
`defineTools({ name, description, fns })` takes the same `ToolFn` catalog as
`glove-scratchpad/fns`, so `fnsFromMcp(conn)` mounts an MCP server as an
`env:` module with no adapter written. A verb puts every answer in the context
window; the same capability as a function lets a script loop and return one line.
Host options gate verbs: `vision` adds `view_image`, `onPresent` adds
`present` (deliver one file from `/out`, with a caption and a media type),
`readOnlyPaths: ["/corpus"]` fences subtrees the agent may read but never edit —
enforced at the single core mutation gateway, so verbs, `env:fs` and adapter
handles are all covered, while `env.mount()` deliberately still writes there.
`glove-env-motion` renders a React scene to video — `render(scene, '/out/x.mp4',
{ durationSeconds })` or `still(scene, '/out/x.png', { frame })`. Mount it WITH
`limits: MOTION_LIMITS`; a frame is a browser screenshot, and a render that
cannot fit `runTimeoutMs` is refused up front rather than timing out. Time is
replaced rather than measured, so two runs of a scene are byte-identical, and
React Native Reanimated scenes render unchanged. `glove-motion-doctor` reports
what a host is missing, with the fix per row.
### glove-egress
```ts
import { egressFns, guardEffectFns, DEFAULT_EGRESS_POLICY, BoundaryMeter } from "glove-egress";
session.registerAll(egressFns(DEFAULT_EGRESS_POLICY)); // assert/count/choose/bucket/report
const guarded = guardEffectFns(catalog, DEFAULT_EGRESS_POLICY, onBlock);
```
Programs must end in a bounded decision; a per-session min-entropy bit budget
caps cumulative disclosure.
### glove-image
```ts
import {
mountImage, InMemoryImageAssetStore, InMemoryImageLibrary,
expandCharacters, expandScenes, styleDirective, llmEnhance,
openrouterImages, UsageMeter,
} from "glove-image";
const meter = new UsageMeter();
await mountImage(glove, {
adapter: openrouterImages(), // OPENROUTER_API_KEY; google/gemini-2.5-flash-image
assets: new InMemoryImageAssetStore(), // BYO ImageAssetStore in production
library: new InMemoryImageLibrary(), // BYO ImageLibraryAdapter in production
model: createAdapter({ provider: "openrouter", model: "openai/gpt-4o-mini", stream: false }),
pipeline: [expandCharacters(), expandScenes(), styleDirective("gouache"), llmEnhance()],
review: { vision: visionAdapter, rounds: 1 }, // optional: describe + self-critique
usage: meter, // optional: read spend host-side
});
```
Folds `glove_image_generate`, `_edit`, `_regenerate`, `_import`, `_describe`,
`_asset_list`, `_assemble`, `_usage`, plus `_character_*` / `_scene_*` CRUD
(writes only when `curate` is true, the default).
Key facts:
- A generation NEVER sends raw text to the image model. It builds a `PromptDraft`
and runs it through the `PromptEnhancer[]` pipeline; `fitToModel()` is always
appended last and clamps the request to the adapter's declared capabilities,
writing each degradation into the trace (surfaced as `data.degradations`).
- Characters/scenes are referenced by NAME in tool ARGS (`characters: ["mira"]`),
never inline syntax. `CharacterDef.appearance` and `SceneDef.setting` are
spliced VERBATIM — `llmEnhance` is instructed not to reword them.
- The model works in asset ids. Bytes never enter tool `data`; thumbnails ride
`renderData`.
- Every derived asset carries a `Recipe` (intent, final prompt, trace, parent,
usage) — `glove_image_regenerate({ asset, tweak })` replays it.
- `glove_image_assemble` needs the OPTIONAL `sharp` peer; it refuses with an
install hint when absent.
- Cost: `ImageUsage { requests, tokens_in, tokens_out, cost_usd? }` per call
(`data.usage`), per asset (`Recipe.usage`), per session (`UsageMeter` +
`glove_image_usage`), plus an `onUsage(source, usage)` callback. OpenRouter
reports real USD.
- Vision is OPT-IN via `review` — without it `describe` returns metadata only
and generations are not critiqued.
### glove-mesh
```ts
import { mountMesh, MeshNetwork, InMemoryMeshAdapter } from "glove-mesh";
await mountMesh(planner, {
adapter: new InMemoryMeshAdapter(new MeshNetwork()),
identity: { id: "planner", name: "Planner", description: "Delegates to specialists." },
});
```
Folds `glove_mesh_send_message`, `_broadcast`, `_list_agents`, `_acknowledge`.
The store must support the inbox methods; inbound messages land in the inbox and
surface on the agent's next turn.
### glove-continuum-signal
```ts
import { agent, z, ContinuumRunner, MemoryAdapter } from "glove-continuum-signal";
export const baker = agent("pizza-baker")
.input(z.object({ orderId: z.string() }))
.triggered() // or .concurrent() for a warm, long-lived subprocess
.timeout(60_000)
.retries(2)
.every("5m").withInput({ orderId: "tick" })
.factory(async (ctx) => buildGlove(ctx));
const runner = new ContinuumRunner({ adapter: new MemoryAdapter() });
runner.registerAgent(baker, import.meta.url);
await runner.start();
```
### glove-mcp
```ts
import { mountMcp } from "glove-mcp";
await mountMcp(runnable, {
adapter: myAdapter, // getActive / activate / deactivate / getAccessToken
entries: [{ id: "notion", name: "Notion", description: "…", url: "https://mcp.notion.com/mcp", tags: ["docs"] }],
clientInfo: { name: "my-app", version: "1.0.0" },
});
```
The only auth seam is `McpAdapter.getAccessToken(id)`. For the spec OAuth flow,
`glove-mcp/oauth` ships an opt-in `runMcpOAuth` runner and reference stores. A
`discovermcp` subagent lets the model activate servers mid-conversation.
## 9. Voice
Cascade (`glove-voice`):
```ts
import { createElevenLabsAdapters } from "glove-voice";
import { useGloveVoice } from "glove-react/voice";
const { stt, createTTS } = createElevenLabsAdapters({
getSTTToken: () => fetch("/api/voice/stt-token").then(r => r.json()).then(d => d.token),
getTTSToken: () => fetch("/api/voice/tts-token").then(r => r.json()).then(d => d.token),
voiceId: "JBFqnCBsd6RMkjVDRZzb",
});
const voice = useGloveVoice({ runnable, voice: { stt, createTTS } });
// voice.mode: "idle" | "listening" | "thinking" | "speaking"
```
Two turn modes: VAD (hands-free with barge-in) and Manual (push-to-talk). In VAD
mode mic audio is speech-gated — it only reaches the STT provider once the VAD
confirms speech, so background noise is never transcribed. Tokens are minted
server-side; API keys never reach the browser.
React Native / Expo: `withNativeAudio({ stt, createTTS, vad })` from
`glove-voice-native`, with `SileroVADNativeAdapter` for on-device VAD.
Realtime speech-to-speech (`glove-voice-s2s`):
```ts
import { RealtimeAgent, createS2SAdapter, s2sDrivenModel } from "glove-voice-s2s";
const rt = new RealtimeAgent({ agent, adapter: createS2SAdapter({ provider: "openai" }) });
await rt.start();
rt.inject("the lookup finished", { respond: true }); // push an async result into the call
```
Adapters declare `mode: "device" | "transport"`. Device owns the mic and plays
the reply (browser only); transport moves PCM only (the mode a server room
needs). On the voice path the provider owns the loop, so: `requiresPermission`
is NOT enforced, `pushAndWait` tools throw (exclude both via `excludeTools`), and
tool calls/transcripts are not persisted — use `RealtimeAgent` events
(`user_said`, `agent_said`, `tool_started`, `tool_finished`).
Avatars: `attachAvatar(rt, avatar)` with `TavusEchoAdapter` or
`AnamPassthroughAdapter` (`glove-voice-avatar`). LiveKit: `LiveKitTransport` +
`attachRealtime(rt, transport)` (`glove-voice-livekit`); with a LiveKit avatar,
set `publishAgentAudio: false` and `{ agentAudio: false }` so the voice is not
published twice.
## 10. Deployment (Glovebox)
```ts
import { glovebox, rule, composite } from "glovebox-core";
import { agent } from "./my-agent";
export default glovebox.wrap(agent, {
base: "glovebox/media", // base | media | docs | python | browser
packages: { apt: ["ffmpeg"] },
storage: { outputs: composite([rule.inline({ below: "1MB" }), rule.localServer({ ttl: "1h" })]) },
});
```
```bash
glovebox build ./glovebox.ts
docker run -p 8080:8080 -e GLOVEBOX_KEY="$(cat dist/glovebox.key)" my-app
```
The deployed server exposes one authenticated WebSocket endpoint per session;
`glovebox-client` speaks to it. Files cross the wire as `FileRef`
(`inline | url | server | s3 | gcs`), never raw bytes.
## 11. Gotchas
- Tool `description` is the interface the model sees — write it for the model,
not for a teammate.
- Whatever `do()` returns costs context on every later turn. Return small,
structured data; put bulk in the display payload or a sandbox file.
- `MemoryStore` is in-process and disappears on restart. Implement
`StoreAdapter` for durable sessions.
- `pushAndWait` suspends the tool. It needs a client that can resolve the slot
— it throws on the S2S voice path.
- Mesh requires a store with inbox support.
- Prefer one `execute_sql` / `execute_js` surface over dozens of folded tools
once a catalog gets large — it measurably improves accuracy and cost.
## 12. Where to read more
- Docs index: https://glove.dterminal.net/docs/intro
- Quickstart: https://glove.dterminal.net/docs/getting-started
- All packages: https://glove.dterminal.net/docs/packages
- Core API: https://glove.dterminal.net/docs/core
- Machine index: https://glove.dterminal.net/llms.txt