# 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 |
| vercel | AI_GATEWAY_API_KEY (or VERCEL_OIDC_TOKEN) | 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-execution | portable browser/sandbox mounts; Station backends in glove-execution/station; model stays on the agent |
| glove-foundry | file-routed application framework: definitions, instances, apps, transmissions, playbooks, schedules, conversations, runtime and inspector |
| 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, GPT-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, dynamic goals (glove-memory/goals; no separate glove-goals package) |
| glove-classifier | classifier models (TypeSafe Jev, LLM-backed, cascades): typed noul/choice/score answers, agent tools, REPL/env/browser/Foundry integrations |
| glove-facts | scoped evidence, revisions and reusable links; preparation through a supplied Glove agent |
| 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-vfs | one Vfs the working environment, memory resources and the REPLs share: backends, mounts, access policy, metadata, search |
| glove-working-environment | persistent sandboxed VFS: scripts, runs, artifacts |
| glove-env-fetch | HTTP requests, VFS downloads/uploads, host network policy and credential aliases |
| glove-env-secret | host keystore, scoped references and pluggable persistence outside VFS snapshots |
| glove-env-documents/-spreadsheets/-images/-slides/-zip/-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-video | agentic video production: temporal prompts, continuity, refs, review gate, resumable flows, 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 |
| 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()); // transient runtime snapshot before each model iteration
// 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.
- **Explicit skips:** fields opt in with `skippable: true` (default false even
for optional schemas). Call `runner.skip("website", "User has no website")`
or `glove_form_revise({ action: "skip", field: "website", reason: "User has no website" })`.
A non-empty reason is required. `skipped` resolves completion, has `ask: false`
and `skipReason` (tool `skip_reason`), supplies no value, and never fires
`onFill`. The builder types skippable values as possibly undefined. Outline
summaries separate `filled` and `skipped`. Skips persist in revision history
as `FormEntry.skipped: { reason }`; undo/redo restore them, retract reopens,
fill/revise replaces them. Fact preparation never overwrites an active skip.
Held skips retain their reason and take effect when applicable. Revisited
steps do not re-ask skips. Do not use silence or a fake "N/A" value as a skip.
- **Skip side effects:** `field.onSkip(ctx)` receives normal executor context
plus `ctx.reason`, supports all effects, and fires on entry into an applicable
skipped state. Repeating a skip or changing its reason does not fire it again;
undo/redo or applicability changes can create a new occurrence. Use
`ctx.idempotencyKey` for external effects; pending skip hook batches persist
before execution and `runner.resumeHooks()` recovers interruptions. Failures
are recorded without rolling back the skip or automatically retrying.
- **Executors** — `field.onFill`, `field.onSkip`, `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 transient model-input-tail line per iteration (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-memory goals — context-sensitive structured progress
Import defineGoalProgram, GoalRunner, GoalAdapter, renderGoalStatus and types
from "glove-memory/goals" (also exported at the root). InMemoryGoalAdapter is
available from "glove-memory/in-memory" and the root. Import useGoalRunner and
buildGoalRunnerTools from "glove-memory/tools" or the root.
useGoalRunner(glove, adapter, { scope: { subject, key, agent? }, actor?, source?,
tools?: { allow?, deny? }, injectStatus?, validateChange?, onChange? }) returns
{ glove, runner, refresh }. The exact scope tuple isolates goal sets; qualify
subject with tenant/conversation/matter identity. Scope can be a thunk, stable
through each request. Tools cannot choose a different scope.
A GoalProgram is { key, goals: [{ key, title, objective, items: [{ key, label,
locked?, retired? }], locked?, retired? }] }. defineGoalProgram validates keys
and definitions. runner.start(program, reason?) creates it idempotently only
when the existing definition is identical. Definitions are persisted JSON;
restarting requires no code registry. No implicit program replacement/reset.
runner.status() returns version, goals, activeGoal and deferred follow-ups;
runner.inspect() returns the saved aggregate; runner.history() returns complete
snapshots with reasons and provenance. runner.update({ goalKey, completed?,
deferred?, declined?, reopened?, reason, ifVersion? }) works on any goal.
Done means completed; deferred/declined settle progression but done stays false.
Deferred work survives whole-program completion and retirement, and can later
be completed or declined. Reopen live items to pending. Unknown/duplicate keys
reject the entire call; repeated dispositions are no-ops.
runner.revise(fullProgram, { ifVersion, reason }) changes remaining goals,
wording, order or items. Same keys preserve progress, even after removal and
reintroduction; new obligations need new keys. A new pending item reopens a
completed goal. Omit or retire obsolete definitions; historical work is retained.
Locked definitions cannot be edited, removed, retired or unlocked. Locks do
not restrict dispositions; enforce domain policy in validateChange (pure,
called before each commit attempt; throw to reject).
GoalAdapter.get(scope) returns a detached snapshot or null. commit(scope, next,
{ ifVersion }) MUST atomically compare-and-set definitions, progress and history;
null means absent, conflicts throw GoalConflictError. Revisions and model updates
require versions. Direct host updates can omit ifVersion for bounded retries
of disjoint progress changes; same-item and definition conflicts are surfaced.
InMemoryGoalAdapter is process-local; production supplies durable storage.
Tools: glove_goal_status, glove_goal_start, glove_goal_update, glove_goal_revise,
glove_goal_history. tools allow/deny only narrows model access, not the host API.
Goals, forms, and context append transient user-role snapshots at the model-input
tail before each iteration, including after tools, without rewriting the system
prompt or saved history. External writes appear next iteration. refresh() runs
preparation, transition recovery, and host configuration. injectStatus:false
permits a custom renderer. Requires glove-core >=4.0.0; runnable proxies forward
addContextProvider and getRuntimeContext. Subscribers receive runtime_context
snapshots. Context providers accept text or native ContentPart[]; media is transient
and runtime_context telemetry replaces media payloads with placeholders.
Message.framework_context marks runtime/inbox provenance without a new
provider role. These entries and skill/compaction markers do not count as real
user turns for tool-result summarization. Inbox reminders follow complete tool
results; adjacent-user merging preserves structured media. Realtime voice
refreshes silently at start and after tools; call
await realtime.refreshContext() after external changes.
onChange runs post-commit; GoalPostCommitError means state WAS persisted.
Forms-to-goals mappings, practice policy and client/matter lookup are host-owned.
Goal lifecycle: configure hooks { onEnter, onComplete, onReopen } in host code.
Each receives { transition, idempotencyKey, goal, status, scope, reason }; mounted
hooks also receive glove and may fold tools or setModel during a turn. Definitions
cannot supply hook code. Transitions are stored atomically in history revisions.
Completion/reopening precede entry; retirement is not completion. A completed
goal may include deferred/declined work, so inspect dispositions for real effects.
GoalAdapter also implements claimTransition(scope,id,{owner,leaseMs}) returning
claimed/completed/busy, settleTransition(scope,id,{owner,state,error?}) with owner
fencing, and getTransitionDispatches(scope). Receipts persist separately from
progress versions. runner.resumeHooks() replays pending effects; hookDispatches()
reads receipts. Mounted runners resume before requests/refresh. Completed effects
stay completed; expired/failed claims can retry, with the SAME idempotencyKey.
Delivery is at least once; hosts deduplicate effects. hookLeaseMs defaults to 60s.
All dispatchers for a scope use the same hooks; adding handlers later replays
unacknowledged historical transitions. A live lease blocks later effects.
Use useGoalRunner(...,{configure({glove,status}) {...}}) for an idempotent projection
of CURRENT progress onto each new runnable, independent of durable effects.
It runs after writes and before every request/refresh, even if injectStatus:false.
Async configure calls serialize; do not mutate goals or call refresh from configure.
fold is additive: guard duplicates. To remove tools or choose constructor options,
read runner.status() and build a fresh Glove for the next request using that state.
### glove-facts — shared evidence and preparation
Import FactStore, InMemoryFactAdapter, FactPreparation, and useFacts from "glove-facts". Scope is an exact subject/context tuple, qualified
by tenant and client/matter. Bind provenance in host code. useFacts registers
record_fact({ fact, urgent? }); model capture stays unverified. FactStore.record
requires source and an operationId; repeated identical operations are idempotent.
Corrections use supersedes: { id, revision }, append a revision and retain history.
Urgent facts surface at capture through onUrgent and remain available afterwards.
Preparation is enabled by supplying a dedicated built IGloveRunnable:
new FactPreparation(facts, { agent: preparationAgent }). No agent means disabled.
Requires glove-facts 0.1.0+ and glove-memory 1.2.0+ for runner integration.
The library appends submit_preparation once on the first run, preserving existing
tools and the system prompt, then runs agent.processRequest, preserving
Glove message persistence, tool traces, usage accounting and subscribers.
There is no raw ModelAdapter/custom inference path or separate enabled flag.
Use a separate preparation agent and store per fact scope, shared by goals/forms
in that scope; never use the workflow agent as its own preparation agent.
No additional user-facing conversation turn is required. Capture alone does not
fan out reconciliation. Starts, relevant commits, mounted turns, or an explicit
runner.prepare() trigger preparation; status/inspect reads do not.
Updating npm packages does not refresh installed coding-agent skills; reinstall
the skill to pick up its workflows.md reference.
Disabled skips automatic inference/claims/prefill without removing captured facts,
accepted links, answers or goal progress. Use runner.prepare() to reconcile now.
Pass preparation: { preparer, rule, eligible? } to either useGoalRunner or
useFormRunner; share one FactStore across both. Goal rule(goal,item) and form
rule(field,compiled) are host-owned allowlists; undefined means manual. A rule
specifies kind information/action/approval/outcome, criteria, optional evidenceKey,
authorizedActors and allowUnverified. Forms validate with the actual field schema;
goal completion proposals must be true. Actions/outcomes require host-attested
verified success for the exact evidenceKey; approval also requires an authorized
actor. Intention is not execution, and preference is not approval.
Preparation retrieves scoped candidates, infers/synthesizes, validates references,
revisions and values, then commits through the runner's authoritative state path.
Conditional eligibility settles before progression and effects; unrelated gates
and checkpoints cannot be skipped by evidence. Prepared status, runtime snapshots,
and tool replies include source-linked synthesis, gaps, urgency and conflicts.
Corrections, changed criteria and contrary values explicitly flag existing work
for review; they do not automatically replace answers or reopen completed actions.
Use ordinary revise/retract/update operations to resolve the review. A fact may
support many requirements: links reference exact revisions, never consume evidence.
Form action/outcome rules may use fulfills: ["field", "step"] only when the
verified outcome covers that field/step effect. This records an evidence receipt
instead of repeating onFill/onComplete. It never suppresses checkpoints or the
form's overall completion hook. Other hooks execute normally.
Production uses a durable FactAdapter.withScope: serialize all writes and consumer
commits across workers, detached reads, durable version + 1 saves, no rollback of
earlier saves on later failure. Do not re-enter the fact store from inference or
commit validation. Proposed claims precede the consumer CAS; accepted claims are
recovered from receipts in goal/form history. FormAdapter must preserve preparation,
claimId, fulfilledHooks, effectId, pendingHooks batches and dispatch effects.
Prepared hooks resume via resumeHooks()/prepare(). External effects are at least
once: deduplicate their stable idempotencyKey. InMemoryFactAdapter is process-local.
### 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-vfs
```ts
import { mountFs, inMemoryFs, hostDirectory, cachedRemote, withAccess, withMeta, hasMeta, hasSearch } from "glove-vfs";
import { fsFns } from "glove-vfs/fns";
import { vfsResources } from "glove-vfs/resources";
const fs = withAccess(
withMeta(mountFs([
{ at: "/", fs: inMemoryFs() },
{ at: "/corpus", fs: hostDirectory("./docs", { mode: "readonly" }) },
{ at: "/memory", fs: await cachedRemote(store, { prefix: `sessions/${id}/` }) },
]), { lexical: true }),
{ rules: [{ path: "/corpus", access: "read", note: "curated upstream" }] },
);
createWorkingEnvironment({ filesystem: fs }); // scripts and verbs
useResourcesCurator(glove, vfsResources(fs, { schema, root: "/memory" })); // memory tools
session.registerFns(fsFns(fs)); // execute_js / _lisp / _python
```
One tree, three consumers: what a script writes to `/memory/notes/x.md` is what
`glove_resources_read` reads, at that path, with no copy or export step.
`Vfs` is nine methods over bytes and absolute paths — `read`, `write` (creates
parents), `rm` (recursive), `mkdir`, `exists`, `stat`, `list`, `files`,
`totalSize`. Summaries, tags, links, provenance and search are OPTIONAL
capabilities detected with `hasMeta(fs)` / `hasSearch(fs)`, never required by the
base type. Ask the tree what it can do; never ask which tree it is.
Backends: `inMemoryFs()` (default), `hostDirectory(dir, { mode })` (copy-on-write;
nothing on the host changes until `commit()`), `cachedRemote(store, { prefix })`
(object storage, BYO `get`/`put`/`delete`/`list`, structural index kept in memory).
Layers each return a `Vfs` and compose outside-in — `withAccess` wraps `withMeta`
wraps `mountFs`, so a policy governs the metadata surface rather than being
bypassed by it. `mountFs` routes by longest prefix; ancestor dirs stay listable but
not writable; `rooted: false` when stored paths must stay absolute. `withAccess`
cascades last-match-wins: a listing FILTERS, a named path REFUSES, `exists` returns
false rather than throwing, a recursive `rm` reaching a fenced path is refused
whole, and `totalSize` reports the entire tree so hiding a subtree buys no budget.
It applies to metadata too: `getMeta` on an unreadable path is refused, and
searches and link lookups filter to visible paths. `withMeta` keeps ONE sidecar at
`/.vfs/meta.json`, hidden from `files()`/`list()` and excluded from `totalSize()`;
search is opt-in (`embedder` or `lexical: true`) and advertised only when real.
Writes never index on the hot path — drain `findNeedingEmbedding()` →
`setEmbedding()` out of band.
`snapshot`/`restore`/`copyTree` call `unwrap()` first, so they serialize what the
backend STORES rather than what the outer layer SHOWS — a snapshot exists to be
restored, so anything it omits is data the restore destroys.
`fsFns(fs)` emits `fs__read`/`ls`/`stat`/`glob`/`grep`, plus
`write`/`mkdir`/`rm`/`mv`/`cp` unless `readOnly`, plus `meta`/`links_for`/`set_meta`
when `hasMeta` and `search` when `hasSearch` — never a call the tree cannot make.
`vfsResources(fs, { schema, root })` SCOPES to a subtree without rewriting paths;
translation would invalidate stored link targets. `runVfsConformance()` from
`glove-vfs/testing` is the backend contract test. Zero dependencies.
### 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 ambient network, host fs or process access.
Hosts explicitly mount external capabilities.
HTTP: mount `fetchFiles({ allowedOrigins, secretStore, credentials })` from
`glove-env-fetch`. Scripts import `request/download/upload` from `env:fetch`;
responses are VFS paths plus status/metadata. Bodies support text, JSON, VFS files,
URL-encoded forms and multipart. Native transport blocks non-public DNS/IPs;
private services need exact `privateNetworkOrigins` host grants. Redirects are
rechecked, HTTPS downgrades refused, and run termination aborts pending HTTP work.
Custom transports must enforce equivalent DNS/IP/TLS protection themselves.
Secrets: mount `secret({ store, names })` from `glove-env-secret`. `env:secret`
exposes list/has/ref without plaintext; get requires allowReveal and writes require
allowWrite. A reference is not an access grant. Host credential aliases resolve
values directly from the store. Scope stores per tenant and re-supply them on
restore: snapshots exclude keystore values and host policy. Default stores are
ephemeral. Never embed credentials in saved source, URLs or run arguments.
Read the mounted `/std/fetch/`, `/std/secret/` docs and `/skills/http-files.md`
and `/skills/secret-references.md` recipes. Call external APIs only inside the
default export, not during module validation. Adapter authors read `ctx.signal`
inside each binding rather than capturing it during create().
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-classifier — structured-decision models
A classifier model does not generate text. It judges a state against typed questions:
noul (yes/no → { noul: p }), choice (label → { choice, confidence, probabilities }),
score (ordered rubric → { score, confidence, probabilities }). Adapter contract:
ClassifierAdapter.classify({ state, questions }, { signal }) → { model, answers, usage }.
import { jev, llmClassifier, cascade, noul, choice, score, gate } from "glove-classifier";
jev() / typesafe(): TypeSafe System One. Reads TYPESAFE_API_KEY, defaults to model jev-latest,
retries 408/429/5xx. Open models on the same /v1/systemone API: kev() (127.0.0.1:8009), laya()
(127.0.0.1:8000), von() (localhost:8000), rizzo() (127.0.0.1:8017) and decider() (127.0.0.1:8000). Each reads
_BASE_URL and _API_KEY. systemOne({ baseURL, model, apiKey? }) covers any other compatible
server. Zero-shot label scorers: huggingfaceZeroShot({ model }) (HF_TOKEN), gliclass({ baseURL }),
labelScorer({ name, score }). llmClassifier({ model }) lets any ModelAdapter answer the same questions.
cascade({ primary, fallback, threshold }) re-asks low-confidence answers. gate(answer, { act, review }) returns "act" | "review" | "escalate".
Agents:
- mountClassifier(glove, { classifier, classifiers?, presets?, sources?, prefix?, concurrency?, resultLimit? })
folds glove_classify, glove_classify_batch, glove_classify_source (host data the agent never reads;
only ids/labels/answers return) and glove_classify_catalog. It returns { addPreset, addSource, removePreset, removeSource, usage }.
- where: { question, choice?, min?, max? }. noul matches when p ≥ min (default 0.5), choice when the label is chosen, score when within min/max.
Code:
- REPL: session.registerAll(classifierFns(jev())) exposes classifier.classify / many / is / pick / rate.
many({ items, questions, where }) runs a whole batch in parallel in one call.
- Working environment: classifierEnv(jev()) from "glove-classifier/env" gives import { many } from 'env:classifier'.
- Browser: withClassifier(browserAdapter, { classifier }) adds browser.judge({ sessionId, questions }).
- Foundry: from "glove-classifier/foundry": classifierPredicate({ classifier, questions, where, state })
goes into defineTransmissionPredicate, and classifyInbound({ classifier, question, events, fallback, minConfidence })
is used as inbound.classify.
### 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-video
`mountVideo(agent, config)` adds provider-neutral video generation, extension,
transformation, review, delivery, libraries, usage, and resumable-flow tools. A
`VideoModelAdapter` declares modes, reference roles, durations, aspect ratios,
resolutions, audio, and candidate limits; `fitVideoToModel` records every
provider-driven adjustment in the asset recipe.
With `review` configured, every generated video is an internal draft.
`glove_video_review` sends the stored video bytes to a separate video-capable
model and stores a score, evidence, issues, and revision prompt.
`glove_video_deliver` refuses any asset without a passing latest review. Multi-
shot flows checkpoint completed nodes and resume without regenerating them.
See `/docs/video` and the recorded run at `/docs/video/gallery`.
### 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`).
GPT-Live (glove-voice-s2s >=0.4.0): select provider "openai-live" in
createS2SAdapter or s2sDrivenModel, with model "gpt-live-1" and backendModel
"gpt-5.6-luna" by default. OPENAI_API_KEY is server-only. The voice model uses
Responses delegation for the same Glove tools; it does not run Foundry's durable
lifecycle. Delegate durable work through a Foundry client tool. Live client
delegation and browser WebRTC are not implemented.
Supply continuous paced mono PCM including silence at 16 or 24 kHz. Listen to
rt.on("transcript", fragment => ...) for { role, delta, startMs, endMs }; there
are no final user_said/agent_said turns. capabilities.transcripts is "continuous"
and speechLifecycle is "host": call notifyPlaybackState(speaking) from real
playback and own avatar utterance boundaries. Neither attachRealtime nor
attachAvatar supplies those boundaries or fills silence gaps. interrupt() mutes
until resumeOutput(); no Realtime VAD, audio commits, or response.cancel. Await
rt.stop() for cumulative usage { seconds, final }; failed finalization rejects.
See /docs/realtime-voice#gpt-live for configuration and compatibility limits.
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. 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.
## 11. 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
# Glove Foundry — condensed reference
Glove Foundry (npm: glove-foundry) is the application framework that runs Glove
agents as a service. You write each agent as a TypeScript file; Foundry provides
persisted agent instances, conversations, a run queue on a managed execution
daemon, a typed client, a REST API, OpenAI-compatible endpoints, schedules,
inbound/outbound integrations, approvals, steering, working environments and a
built-in inspector + WebChat. Every agent is still a normal glove-core agent.
Docs: https://glove.dterminal.net/foundry/docs · Tutorial: https://glove.dterminal.net/foundry/docs/tutorial ·
API reference: https://glove.dterminal.net/foundry/docs/reference · FAQ: https://glove.dterminal.net/foundry/docs/faq
Source: https://github.com/porkytheblack/glove/tree/main/packages/glove-foundry
Canonical example: https://github.com/porkytheblack/glove/tree/main/examples/foundry-agent
## 1. Install and run
```bash
npx glove-foundry init my-agents # interactive wizard (Clack)
npx glove-foundry init my-agents --template minimal --package-manager pnpm --yes --install
cd my-agents && pnpm dev # runtime + inspector on http://127.0.0.1:4141
```
- Node 22+ required; 22.13+ for SQLite memory adapters.
- Templates: travel-concierge (default; keyless demo model, one example of every
convention) and minimal (one agent, one tool; needs OPENROUTER_API_KEY in .env.local).
- --target standalone|nextjs (Next.js apps are detected), --package-manager
pnpm|npm|yarn|bun, --yes/--no-interactive, --interactive, --install/--no-install, --help.
- Inside a project: glove foundry dev | start | help; run options --root, --port,
--host, --no-watch. dev watches agents/config and regenerates .foundry/routes.d.ts.
- The wizard never asks for API keys. Starter storage is in-memory and resets.
## 2. Project layout and file conventions
```text
agents//agent.ts defineAgent — the route id is the folder path
agents//composition.ts composeAgent(...) catalogue (ordinary module)
agents//tools/*.tool.ts defineSharedTool
agents//apps/*.app.ts defineApp
agents//transmissions/*.transmission.ts defineTransmission
agents//events/*.event.ts defineTransmissionEvent
agents//predicates/*.predicate.ts defineTransmissionPredicate
agents//actions/*.action.ts definePlaybookAction
agents//connections/*.connection.ts defineConnection
agents//mcp/*.mcp.ts defineMcp
agents//memory/*.memory.ts defineMemory
agents//layers/*.layer.ts defineLayer
agents//subscribers/*.subscriber.ts defineSubscriber
foundry.application.ts defineApplication — data adapter, accounts, routes, auth
foundry.config.ts defineConfig (from glove-foundry/config) — server, execution
.foundry/routes.d.ts generated FoundryRoutes type — do not edit
```
- Each convention file default-exports one definition; its filename is its id.
Nested paths nest ids: tools/calendar/today.tool.ts -> "calendar/today" (a bare
tool body there is exposed to the model as calendar__today).
- Never add an id to a file-routed definition (the glove-foundry/eslint preset rejects it).
In code, pass imported values (install(helpdesk)), not strings.
- Schedules are NOT auto-discovered: keep defineSchedule values in ordinary modules
and return them from the agent's schedules field.
- String ids are correct only for runtime data: instances, conversations, runs,
accounts, routes, bindings, and at HTTP/database boundaries.
## 3. The model: definition, instance, conversation, run
- Definition (code): what an agent of this kind can assemble.
- Instance (data, FoundryDataAdapter): { id, definitionId, workspaceId,
provisioningKey?, context, installations, playbooks, createdAt, updatedAt }.
Created by createAgent / POST /api/agent-instances / defineAgentInstance seeds /
subscriptions; edited with configureAgent / PATCH /api/agent-instances/:id.
- Conversation (data): owned by one instance; an instance may own many.
- Run: one message processed by a freshly assembled agent. Every run reloads the
instance + conversation and resolves every definition field again, so instance
edits apply on the next message without a deploy.
- Every run executes in a fresh child process of the managed execution daemon.
Anything held only in memory — new MemoryStore(...), in-memory memory adapters,
MemoryFoundryDataAdapter writes made by agent tools, module-level variables —
does not survive to the next message. Durable state must live outside the process.
- There is no API to delete an instance.
## 4. defineAgent
```ts
import { createAdapter } from "glove-core/models/providers";
import { defineAgent } from "glove-foundry";
import { z } from "zod";
import { FileStore } from "../../lib/file-store.js"; // durable transcript, see section 16
export default defineAgent({
description: "Answers questions about orders",
model: () => createAdapter({ provider: "openrouter", model: "openai/gpt-4.1-mini", stream: true }),
store: ({ conversationId }) => FileStore.open(conversationId),
systemPrompt: (_agent, ctx) => `You are support for ${ctx.agentInstance.context.company}.`,
tools: [{ // always-on tools: plain Glove tools
name: "lookup_order",
description: "Find an order by id",
inputSchema: z.object({ orderId: z.string() }),
async do({ orderId }) { return { status: "success", data: { orderId, state: "shipped" } }; },
}],
});
```
Every assembly field is a literal or a resolver (agent, ctx) => value | Promise.
Fields: description, tags, components, store, model, systemPrompt, displayManager,
serverMode, maxRetries, maxConsecutiveErrors, compactionLimit,
compactionInstructions, maxTurns, enableToolResultSummary, tools, hooks, skills,
subagents, memory, goals, facts, forms, contextProviders, inboxes (always a
function), subscribers, layers, calls, schedules, playbooks, mesh,
workingEnvironment, repl, configure(agent, ctx), build(agent, ctx),
spawn(agent, ctx, message), run(agent, ctx), mcpAdapter, accountSessions.
An agent definition never declares request input/output — Foundry owns
FoundryRequest/FoundryResult.
AgentAssemblyContext: definitionId, agentId (instance id), conversationId,
workspaceId, runId, request, agentInstance, conversation, activations, data,
message (native Glove Message), messageInput, messageText, history, messages,
installations, store, subscriber, controls { signal, emit, commands }.
configure, run, spawn and call handlers get a different execution context:
identity ids, request, message, history, glove, emit, signal, installations,
onCleanup, invoke(callName, input), workingEnvironment, vfs, repl and
goals/facts/forms handles — but not agentInstance, conversation, data, store or
controls (use ctx.emit instead of controls.emit there). A run handler executes
after assembly and receives the assembled runnable; call ctx.defaultRun() for the
normal loop.
## 5. Catalogue vs installation (most common mistake)
composeAgent(...) + components only makes shared tools, apps and MCP servers
AVAILABLE. They mount only on instances that install them:
- install(def, config?, { account? }) in defineAgentInstance / createAgent({ installations })
- client.install(agentId, { kind, id, config }) / client.uninstall(...)
- PUT /api/installations {"agentId","kind":"tool"|"application"|"mcp","id","accountId?","config?"}
(DELETE with the same body uninstalls). The inspector displays installations but
cannot change them. install(def) only works inside the Foundry process (file ids
are bound by discovery); remote clients send plain { kind, id, config } records.
Tools every instance should have go in defineAgent's tools field. Memory profiles,
layers and subscribers are definition surfaces, not installations: list them in
components AND select them in the memory/layers/subscribers field, or the run fails
with "unknown ...". Installation config is validated when a run assembles, so an
invalid config fails the next run, not the install call. Installing an MCP server
requires the agent definition to set mcpAdapter. Config is typed by the definition's zod schema:
install(helpdesk, { queue: 42 }) is a TypeScript error and is re-validated at runtime.
```ts
// agents/support/tools/refunds.tool.ts
import { Effect } from "effect";
import { defineSharedTool } from "glove-foundry";
import { z } from "zod";
export default defineSharedTool({
description: "Issue refunds up to a per-instance limit",
config: z.object({ limitUsd: z.number() }),
create: ({ config }) => Effect.succeed({
name: "issue_refund",
description: `Refund an order (max $${config.limitUsd})`,
inputSchema: z.object({ orderId: z.string(), amountUsd: z.number() }),
requiresPermission: true, // pauses on an approval record in Foundry
async do({ orderId, amountUsd }: { orderId: string; amountUsd: number }) {
return { status: "success" as const, data: { orderId, amountUsd } };
},
}),
});
// A static tool can use { description, tool: {...} } instead of create.
```
defineMcp({ description, config?, entry }) — entry is an HTTP { url } or
{ transport: { kind: "stdio", command, args } } plus includeTools/excludeTools
globs, resources, prompts, connectTimeoutMs, requestTimeoutMs, idleTimeoutMs,
maxLifetimeMs. Auth headers, tokens and stdio env come from the agent's
mcpAdapter at connect time, never from config.
defineMemory / configureMemory, defineLayer / configureLayer (setup receives the
native glove), defineSubscriber, defineCall({ name, input, output, handler,
exposeToAgent? }) (exposed as a tool by default; callable via ctx.invoke).
## 6. Apps, transmissions and the outside world
- defineTransmissionEvent({ direction: "inbound" | "outbound", description })
- defineTransmissionPredicate({ match: (event, parameters) => Effect })
- defineTransmission({ name, description, account: { required, metadata },
capabilities, events, inbound: { config, event, classify, predicates },
outbound: { config, input, output, adapter: { deliver(input, context) }, observe? } })
— Effect Schema types flow into account metadata, route config, events, input, output.
- defineApp({ description, config?, inbound, outbound, connections?, install? }) —
install returns Effect<{ tools }>. Installing an app mounts its outbound
transmissions as validated tools; delivery runs in the parent runtime.
- defineConnection — a supervised long-lived inbound worker: connect(ctx) uses
ctx.ready() and ctx.receive({ route, eventId, threadKey, conversationKey?,
conversationScope?, awaitCompletion?, raw }). Started only when an installed app
has an active inbound playbook or subscription.
- Topology is data: defineAccount({ id, transmission, externalAccountId, label,
accessRef, metadata }), defineInboundRoute / defineOutboundRoute({ id,
transmission, account, visibility, enabled, config }), defineBinding; register
them in defineApplication({ accounts, routes, bindings }).
- Outbound delivery requires an enabled defineBinding for the instance (registered in
defineApplication bindings); without one delivery fails with "agent has no enabled
bindings". Transmission ids are global across agents.
- Fire inbound events: POST /api/transmissions/:routeId/fire or client.dispatchInbound.
Delivery is claimed idempotently by routeId:eventId.
- Credentials: Foundry never does OAuth, stores secrets, refreshes tokens or picks
accounts. accessRef is opaque; your accountSessions adapter resolves it;
outbound adapters call context.withAccountSession(operation, use).
- Classifier triage: classifyInbound and classifierPredicate from
glove-classifier/foundry (see https://glove.dterminal.net/docs/classifier#foundry).
## 7. Playbooks and provisioning
composePlaybook({ name, transmission, match: { event, routes, predicate: {
definition, parameters } }, directives: [{ action, instruction, parameters? }],
applications, outbound?: [{ route, application, account, event, instruction }],
serialization? }) is called at runtime (usually returned from the agent's playbooks
resolver) and persisted on the instance as id-based data. A playbook can bind only
transmissions owned by installed apps.
Playbook subscriptions (runtime.putPlaybookSubscription / client.putPlaybookSubscription /
PUT /api/playbook-subscriptions) are evaluated even when no instance exists and can
provision agents: singleton, per-thread, per-event, existing, or custom (the
application's provisioner). provisioningKey makes provisioning atomic.
Playbooks and schedules returned by a definition's resolvers are written onto an
instance during its first run, so a brand-new instance ignores inbound events until
it has run once; subscriptions and client.setAgentPlaybooks apply immediately.
## 8. Built-in agent tools (mounted on every Foundry agent)
| Tool | Purpose |
| glove_foundry_spawn | Invoke an agent now ({ definitionId?, agentId?, conversationId?, message, payload? }) |
| glove_foundry_schedule | Create a future/recurring activation; timing { kind: "at", at } / { kind: "after", duration } / { kind: "every", interval } / { kind: "cron", expression, timezone? } |
| glove_foundry_schedules | list / update / pause / resume / cancel this instance's activations |
| glove_foundry_sleep | Record a wake-up for the same instance + conversation ({ kind: "for", duration } or { kind: "until", at } in UTC ISO with Z); the current run ends and a new run starts at wake time |
| glove_foundry_background | Queue a run on the same instance + conversation; with reconvene (default true) it waits up to 60 s and posts a resolved background.reconvene item to the shared inbox (it does not wake the conversation) |
| glove_foundry_transmit | Deliver an outbound event through a route declared by this instance's playbook |
| glove_foundry_conversations | List this instance's conversations |
| glove_foundry_workspace | Read or write a shared workspace value |
| glove_foundry_shared_inbox | List or post shared inbox items |
| glove_foundry_tasks | List or create shared tasks |
| glove_foundry_environment | List non-secret environment metadata |
Durations: "30s", "20m", "2h", or Effect forms like "20 minutes".
defineSchedule({ name, description?, timing, message, payload?, enabled? }) values
returned from the schedules resolver are reconciled into persisted activations
(see the Automations page of the inspector, GET /api/activations). Activations are
persisted before arming and reconstructed on startup with a durable adapter.
Never use setTimeout for agent work.
## 9. Conversations and shared work
One instance owns many conversations (createConversation, conversations,
updateConversation, conversationTranscript — transcript.persisted is false when
no store is configured). Workspaces carry entries (putWorkspaceEntry;
compareAndSetWorkspaceEntry for optimistic coordination), a shared inbox, tasks
and non-secret environment values. Pass artifact paths, not copied documents.
## 10. Working environment and REPL
defineWorkingEnvironment({ options, persistence? }) mounts glove-working-environment
(guarded VFS, scripts, checkpoints, export). It closes after every run; add
foundryDataEnvironmentPersistence({ scope }) or a native persistent Vfs to keep files.
defineRepl({ language: "javascript" | "python" | "lisp", session, mount?,
programmaticTools? }) — one REPL per run. programmaticTools.select projects exact
live tools into the sandbox (maxCalls default 50); permission-gated tools are refused
there. Add document/spreadsheet/slide/image/media/fetch/secret adapters from
glove-env-* packages. Browsers and sandboxes: mountBrowser / mountSandbox from
glove-execution inside configure, with ctx.onCleanup.
## 11. Multi-agent
defineSubagent (isolated child; tools can be a least-privilege projection of the
parent built from parent.tools — there is no projectTools export; a subagent's
layers and subscribers mount per invocation and are disposed when its request
ends), defineCall (typed function), glove_foundry_spawn (separate run),
glove_foundry_background (reconvene via inbox), mesh field (glove-mesh),
playbook subscriptions (provision a workforce), workspace/inbox/tasks for
coordination. Voice hosts (glove-voice-s2s) delegate durable work to an
instance via the typed client.
## 12. Client and HTTP API
```ts
import { createFoundryClient } from "glove-foundry/client";
import type { FoundryRoutes } from "./.foundry/routes.js";
const foundry = createFoundryClient({ baseUrl: "http://127.0.0.1:4141" });
const agent = await foundry.agent("support").create({ workspaceId: "acme", context: { company: "Acme" } });
const conversation = await foundry.createConversation(agent.id);
const run = await foundry.send(agent.id, conversation.id, "Where is order 1042?");
const done = await run.wait({ timeoutMs: 120_000 }); // wait() gives up after 60 s by default
// also run.cancel(), run.steer(text), run.events()
```
agent(route).request(...) posts to /api/agents/:route/runs and needs agentId,
conversationId and workspaceId as well as a string message; prefer send().
Native endpoints (selection): GET /health, /health/detailed, /api/manifest,
GET|POST /api/agent-instances, PATCH /api/agent-instances/:id,
GET|POST /api/conversations, GET|POST /api/conversations/:id/messages,
PATCH /api/conversations/:id, GET /api/runs, GET /api/runs/:id,
POST /api/runs/:id/cancel, POST /api/runs/:id/steer, GET /api/events (SSE with
Accept: text/event-stream), GET|PUT|DELETE /api/installations, GET /api/approvals,
POST /api/approvals/:id, /api/workspaces/:id/entries|inbox|tasks|environment,
/api/playbook-subscriptions, /api/activations, /api/transmissions,
POST /api/transmissions/:routeId/fire, /api/accounts, /api/routes, /api/bindings,
/api/application-connections, /api/capabilities, /api/surfaces.
Automation API: POST /v1/runs, GET /v1/runs/:id, GET /v1/runs/:id/events,
POST /v1/runs/:id/stop, POST /v1/runs/:id/steer, GET /v1/capabilities,
GET /v1/approvals, POST /v1/approvals/:id.
JSON bodies are limited to 1 MB by default (413 when exceeded); raise
server.messageBodyBytes for media. Runs, events and pending approvals are held in
the Foundry process's memory and are lost on restart; /health and /health/detailed
return 200 when ok is true and 503 with the same body when it is false
(client.health() returns the body either way). Activations are read-only over HTTP.
Installing an unknown id returns 400.
## 13. OpenAI-compatible endpoints
GET /v1/models lists instances (id = instance id). POST /v1/chat/completions and
POST /v1/responses support streaming and image input. model = an instance id, or a
definition route that has exactly one instance (otherwise 404); instances are never
created implicitly. Continue a conversation by sending the same user,
conversation_id, or x-foundry-conversation-id header (returned on every response);
without one, each request starts a new conversation. On a new conversation the
prior messages in the request are folded into the first turn; on an existing one
only the latest user message is sent because Foundry holds the history (so the
agent needs a durable store). Responses carry no usage block. Tools run server-side inside Foundry; client-sent
tools, sampling parameters and previous_response_id are ignored, and the developer
role is rejected.
## 14. Approvals and steering
A Glove tool with requiresPermission: true (or a function of its input), or an
outbound transmission with requiresPermission, pauses the run on an approval
record: status pending | approved | denied | expired | cancelled; identity = instance
+ conversation + run + tool + serialized input; expires after 5 minutes; fails
closed (deny) on expiry, cancellation or channel loss. Approval records live in a
private temp directory of the Foundry process: they do not survive a restart, and
the mechanism does not engage if defineAgent supplies its own displayManager. Resolve with
client.approvals({ runId, status: "pending" }) and client.resolveApproval(id,
"approve" | "deny"), the /api/approvals or /v1/approvals routes, or the inspector.
Steering (handle.steer(message), /api/runs/:id/steer, /v1/runs/:id/steer, WebChat
Redirect) is interrupt-and-restart: cooperative cancel, a wait (at most 10 s) for
the cancelled run's process to finish unAbortable tools and cleanup, then a
replacement run in the same conversation with lineage. It never injects text mid
side effect.
## 15. Inspector and observability
glove foundry dev serves the inspector at the server URL: Overview, Agents,
definition and instance pages, Chat (WebChat over exact stored transcripts, with
attachments, Stop and Redirect), Runs (filters in the URL: ?status=&source=&q=),
run detail (phase spine + event trace), Automations, Integrations, Workspaces.
⌘K search, j/k navigation, c to start a run. Events stream from /api/events;
custom sinks use defineSubscriber or a FoundryObservabilityAdapter. Use
controls.emit({ type, data }) for safe work notes; never emit secrets or hidden
reasoning. The event envelope carries the definition id and run id (not instance,
conversation or workspace ids). Only outbound transmission payloads are redacted by
default — tool results and model text are stored as-is. A custom
FoundryObservabilityAdapter can be supplied only when hosting FoundryRuntime in code;
the CLI uses the in-memory adapter.
## 16. Persistence — four separate boundaries
1. Foundry runtime data: FoundryDataAdapter (MemoryFoundryDataAdapter in dev;
FileFoundryDataAdapter({ file, agents?, conversations?, subscriptions? }) for one
host — advisory lock + atomic rename; a transactional adapter for many hosts).
2. Conversation transcripts: the agent store / application conversationStore.
A plain MemoryStore starts empty on every run (each run is a new process), and
GET /api/conversations/:id/messages then reports persisted: true with no
messages — persisted only means a store factory exists. Minimal durable store
for one host:
```ts
// lib/file-store.ts
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { MemoryStore, type Message } from "glove-core";
export class FileStore extends MemoryStore {
private constructor(id: string, private readonly file: string) { super(id); }
static async open(id: string, directory = ".data/transcripts"): Promise {
await mkdir(directory, { recursive: true });
const store = new FileStore(id, join(directory, `${encodeURIComponent(id)}.json`));
const saved = await readFile(store.file, "utf8").then(JSON.parse, () => [] as Message[]);
await MemoryStore.prototype.appendMessages.call(store, saved); // restore without re-saving
return store;
}
override async appendMessages(messages: Message[]): Promise {
await super.appendMessages(messages);
await writeFile(`${this.file}.tmp`, JSON.stringify(await this.getMessages()));
await rename(`${this.file}.tmp`, this.file);
}
}
```
Use a database-backed StoreAdapter for several hosts. (glove-sqlite is deprecated.)
3. Structured memory: glove-memory adapters, e.g. createSqliteMemoryAdapters({ file,
namespace, schema }) from glove-memory/sqlite (Node 22.13+).
4. Working-environment files: foundryDataEnvironmentPersistence or a persistent Vfs.
Persisting one does not persist the others. The execution queue is in-memory;
activations are reconstructed on startup but interrupted live runs are not resumed.
## 17. Application, config and security
defineApplication({ name, data, conversationStore, accounts, routes, bindings,
requestAuthorization, provisioner, services, daemon }).
defineConfig({ agentsDir, applicationFile, server: { host, port, messageBodyBytes },
execution: { pollIntervalMs, maxConcurrent, maxAttempts, retryBackoffMs },
observability: { maxEvents }, branding, strictFileRoutes }) — unknown keys are
rejected. Environment files: .env is loaded before .env.local and an already-set
variable is never overwritten. Run one runtime per data store: two runtimes on the
same store arm every schedule twice. Foundry binds to loopback by default and refuses a non-loopback
host unless requestAuthorization ({ identifier, challenge, authorize(request) =>
Effect }) is supplied; the typed client sends matching headers via
authorization: { identifier, headers }. Put TLS and rate limits in front.
glove foundry start runs without watching.
## Goals, facts, and form preparation
Foundry exposes typed lazy goals, facts, forms and contextProviders fields, either
in defineAgent or as agent.ts named exports. defineGoals/defineFacts/defineForms
are pure configuration helpers; native runners own behavior. Execution hooks get
ctx.goals/ctx.facts/ctx.forms handles. Conversation scope is default; scope:
"instance" shares explicitly. foundryGuidanceSubject derives the same ownership
key outside assembly. A goal program seeds only absent state; later revision is
native CAS with a reason. Registries expose forms without starting all of them.
facts.preparationAgent is a separate built runnable with its own scoped store;
goals/forms preparation supplies rule and eligible callbacks. Foundry constructs
the native shared preparer and rejects subject mismatches or missing agents.
contextProviders resolves a list of native read-only providers, re-read each model
iteration and removed on cleanup. glove-memory/sqlite now includes durable goals,
facts and forms. Facts serialize per database through a separate OS-owned SQLite
lock while saves commit independently. Use local volumes, not network filesystems.
The inspector guidance card shows metadata from the latest observed run, not a
live database view. See /foundry/docs/guidance and the guided-intake example.
Dynamic goals are exported by glove-memory/goals (glove-memory 1.2.0+), not a
standalone glove-goals package. Shared evidence is glove-facts 0.1.0+.
Use useGoalRunner/useFormRunner from glove-memory/tools to mount workflow tools
on a conversational Glove runnable. useFacts from glove-facts mounts record_fact;
imports alone do not mount tools. Scope fact storage by subject and context.
With glove-core >=4.0.0, goals/forms/pinned context append live transient user-role
snapshots at the model-input tail before each iteration, leaving system prompt
and saved history unchanged. Runnable proxies forward addContextProvider and
getRuntimeContext; runtime_context subscriber events expose resolved snapshots.
Realtime voice refreshes silently at start/after tools; external changes require
await realtime.refreshContext(). Voice sessions retain superseded snapshots.
new FactPreparation(facts, { agent: preparationAgent }) enables preparation.
Supply a dedicated built IGloveRunnable with its own scoped conversation store
and the application's tracing subscribers. The library appends submit_preparation
once at first use and runs processRequest, preserving tool execution, model
usage and persisted history. Do not use a direct ModelAdapter call or the
conversational agent itself. Without a preparation agent, capture and ordinary
workflow operations remain available and automatic preparation is disabled.
Pass preparation: { preparer, rule, eligible? } to the goals/forms runner.
Host rules allowlist requirements, schemas/evidence policies validate proposals,
and ordinary goal/form commits remain authoritative. One fact can support both
consumers. Corrections flag existing work for review. Capturing a fact alone
does not trigger all workflows; preparation runs on starts/relevant commits,
before mounted turns, or explicit runner.prepare(). Production adapters must
preserve evidence receipts and prepared-form recovery state.
Use Foundry’s lazy goals, facts, forms and contextProviders fields for native
guidance assembly. Browser/sandbox capabilities use explicit configure mounts;
they do not add browser or sandbox fields to agent definitions. Detailed workflow skill:
https://github.com/porkytheblack/glove/blob/main/.claude/skills/glove/workflows.md
## Browser and sandbox mounts
The optional glove-execution package provides mountBrowser(agent, { adapter })
and mountSandbox(agent, { adapter }). glove-core has no browser/sandbox dependency.
Station adapters live in glove-execution/station. Mount explicitly in configure;
register generic context.onCleanup teardown immediately after acquiring resources.
Browser scripts use execute_browser and sandbox scripts use execute_sandbox.
Page evaluation is separate from workflow scripting. Screenshots reach the model
as native images after tool results. Retained scopes grant resource IDs across runs;
script variables are run-scoped.
Runnable example: https://github.com/porkytheblack/glove/tree/main/examples/foundry-operator
Guide: https://glove.dterminal.net/foundry/docs/browser-and-sandbox
Operator runs locally on port 4243 with OpenRouter, Station, Steel profiles and a Docker
workspace. Human sign-in belongs to the user; Telegram sign-in has not been verified
end to end. Messaging actions require user direction in the application prompt. Port 3000 sandbox services preview through a separate-origin GET bridge.
No host keys, folders or Docker socket are mounted into the sandbox.
Foundry owns one managed local Station daemon for agent jobs and optional resources.
Configure application.daemon with stationDaemon({ stationId, resources, onReady })
from glove-foundry/station. The resource factory runs once inside that daemon and
returns optional browser and sandbox adapters. Agent capabilities still use mounts.
The private onReady connection is { url, stationId, token }; Station 3 requires
operator/admin scope for this resource API. Never expose it in prompts, manifests,
telemetry or frontend code. Programmatic setup with a daemon adapter supplies
applicationFilePath so the managed process can load the resource factory.
Factories release partially acquired resources if they throw before returning;
Foundry owns successfully returned providers through shutdown and startup failure.
Omit daemon for jobs only, or return only browser or sandbox. Each runtime owns
one local Station. Memory defaults to the agent conversation; cross-scope recall,
checkpoint policy and eager/lazy resource acquisition belong to the application.
Operator selects a native defineMemory profile with SQLite context, episodic, entity
and resource adapters, scoped per workspace/instance/conversation. Pinned unfinished
tasks and preferences refresh before each model iteration. Exact current requests
are re-injected independently of compaction; summaries preserve outcomes, constraints,
uncertainty and next actions. The store persists native tasks and inbox items and
measures current context pressure separately from cumulative usage. verify:memory
tests real-model memory writes and recall after forced compaction in an isolated
conversation, without modifying the user's pinned memory.