defineAgent and per-run assembly
defineAgent describes how to build a Glove agent for one run. Almost every field can be a function of the instance, the conversation and the new message, so one definition can serve many customers with different prompts, models and tools — without branching inside the prompt.
A complete agent in one file
Save this as agents/support/agent.ts and the route support exists. Every instance of it gets the lookup_order tool.
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 Persistence
export default defineAgent({
description: "Answers customer questions about orders",
store: ({ conversationId }) => FileStore.open(conversationId),
model: () =>
createAdapter({
provider: "openrouter",
model: process.env.OPENROUTER_MODEL ?? "openai/gpt-4.1-mini",
stream: true,
}),
systemPrompt: "You are a concise, friendly customer-support agent.",
tools: [
{
name: "lookup_order",
description: "Look up an order by its id",
inputSchema: z.object({ orderId: z.string() }),
async do({ orderId }) {
// your code: query the order system
return { status: "success" as const, data: { orderId, status: "shipped" } };
},
},
],
});defineAgent validates the definition when the module loads:
- It must define
model,runorspawn. - Defining
modelrequiressystemPrompt. inboxesmust be a function.inputandoutputkeys are rejected: requests and results use Foundry's fixed contracts. Userequest.payloadordefineCallfor typed data.
Each run executes in its own agent job subprocess. A MemoryStore, an in-memory memory adapter or a module-level variable therefore does not carry over to the next run. Return a durable StoreAdapter from store when history must survive between turns; see Persistence.
The value-or-resolver rule
Lazy fields have the type FoundryResolver<T>:
type Resolvable<T> = T | Promise<T> | Effect.Effect<T, unknown, never>;
type FoundryResolver<T, TInput> =
| T
| ((agent: FoundryAgentDefinition, ctx: AgentAssemblyContext<TInput>) => Resolvable<T>);- A literal is used as is for every run. Any function is treated as a resolver and called once per run.
- A resolver may return the value, a
Promise, or anEffectwith no requirements (Foundry runs it withEffect.runPromise). A literal cannot be a Promise. - The first argument is the frozen definition itself (
agent.id,agent.description), not a Glove instance. - Resolvers in the same assembly step run concurrently and see the same read-only context; do not rely on order between them. A thrown error fails the run.
store,inboxes,mcpAdapter,configure,build,spawnandrunhave their own signatures, listed below.
Every field
Identity, catalogue and adapters
| Field | Type (resolved value) | Lazy | Purpose |
|---|---|---|---|
description | string (required) | no | Shown in the manifest and inspector; default mesh identity description. |
tags | readonly string[] | no | Labels copied into the manifest (GET /api/agents). |
components | composeAgent(...) | no | Catalogue of shared tools, apps, MCP servers, memory profiles, layers and subscribers this definition may use. Nothing in it mounts by itself. |
store | ({ definitionId, agentId, conversationId, workspaceId }) => StoreAdapter | Promise | own signature | Conversation store for history. Falls back to the application's conversationStore; with neither, history is empty. |
mcpAdapter | (ctx) => Effect<McpAdapter> | per run | Auth headers, tokens and stdio env for installed MCP servers. Required once any MCP is installed. |
accountSessions | { identifier, withSession(request, use) } | per run | Gives installers withAccountSession for installations that selected an account. |
Glove configuration
| Field | Type (resolved value) | Lazy | Purpose |
|---|---|---|---|
model | ModelAdapter | yes | Required unless run or spawn is defined. |
systemPrompt | string | yes | Required whenever model is defined. |
displayManager | DisplayManagerAdapter | yes | Default is Foundry's approval display manager, which turns permission prompts into approval records. Overriding it sends those prompts to your display manager instead. |
serverMode | boolean | no | Glove serverMode. Foundry's default is true. |
maxRetries | number | no | Tool execution retries (Glove default 3). |
maxConsecutiveErrors | number | no | Passed through to Glove's maxConsecutiveErrors option. |
compactionLimit | number | yes | Context tokens before compaction (Glove default 100,000). |
compactionInstructions | string | yes | Default: “Preserve goals, decisions, unresolved work, tool results, and pending inbox items.” |
maxTurns | number | yes | Model turns allowed per request (Glove default 120). |
enableToolResultSummary | boolean | no | Summarize older tool results in the prompt (Glove default false). |
Mounted surfaces
| Field | Type (resolved value) | Lazy | Purpose |
|---|---|---|---|
tools | GloveFoldArgs[] | yes | Always-on tools for every instance. |
hooks | { name, handler: HookHandler }[] | yes | Glove hooks triggered by /name directives. |
skills | DefineSkillArgs[] | yes | Glove skills. |
subagents | DefineSubAgentArgs[] (defineSubagent) | yes | Nested delegates; see Multi-agent. |
memory | (defineMemory | configureMemory(...))[] | yes | Memory profiles to mount. Each must also be in components. |
goals / facts / forms | defineGoals / defineFacts / defineForms | yes | Guidance runners; handles appear on the execution context. |
contextProviders | RuntimeContextProvider[] | yes | Transient context re-read before each model iteration. |
inboxes | (agent, ctx) => InboxItem[] | must be a function | Loads Glove inbox items into the store. Requires an inbox-capable store. |
subscribers | (defineSubscriber | SubscriberAdapter)[] | yes | Event observers. Foundry subscribers must be in components; plain adapters need not be. |
layers | (defineLayer | configureLayer(...))[] | yes | Setup functions with native Glove access. Must be in components. |
calls | defineCall(...)[] | yes | Typed functions, exposed as tools unless exposeToAgent: false; callable with ctx.invoke. |
schedules | defineSchedule(...)[] | yes | Desired recurring work, reconciled into activations every run. |
playbooks | composePlaybook(...)[] | yes | Inbound-event policy persisted on the instance. Omit to keep the instance's own playbooks. |
mesh | { adapter: MeshAdapter, identity? } | yes | Peer messaging via glove-mesh. |
workingEnvironment | defineWorkingEnvironment(...) | yes | Persistent VFS and named scripts. |
repl | defineRepl(...) | yes | One JavaScript, Python or Lisp REPL, optionally with programmatic tool calling. |
Installable shared tools, apps and MCP servers are not fields: they live in components and mount only when the instance installs them. See Tools, MCP & memory. Guidance fields are covered in Goals, facts & forms, schedules and playbooks in Playbooks, schedules & sleep, and workingEnvironment/repl in Working environments.
Lifecycle hooks
| Field | Type (resolved value) | Lazy | Purpose |
|---|---|---|---|
configure | (agent: IGloveRunnable, ctx: FoundryExecutionContext) => Resolvable<void> | hook | Mount anything onto the assembled agent; register teardown with ctx.onCleanup. |
build | (agent, ctx: AgentAssemblyContext) => Resolvable<IGloveRunnable | undefined> | hook | Wrap or replace the runnable. Returning undefined keeps it. |
spawn | (agent, ctx: FoundryExecutionContext, message) => Resolvable<unknown> | hook | Build a custom execution for one message. A returned object with processRequest() or run(message, signal) is executed; any other value is the result. |
run | (agent, ctx: AgentHandlerContext) => Resolvable<unknown> | hook | Own the whole execution. Call ctx.defaultRun() for the normal Glove loop. |
id and handler still type-check but are deprecated: the file path owns identity, and run replaces handler. An agent.ts may also use named exports (export const description, export const run, …) instead of a default defineAgent.
The assembly context
Every resolver and build receive an AgentAssemblyContext:
| Field | Type | Notes |
|---|---|---|
definitionId | string | The route, e.g. support |
agentId | string | The instance id |
conversationId | string | |
workspaceId | string | |
runId | string | |
name / mode | string / "agent" | Internal execution name (foundry_support) and mode |
request | FoundryRequest | { agentId, conversationId, workspaceId, message, payload?, context?, source? }; input is the same object |
agentInstance | AgentInstance | Persisted record: context, installations, playbooks… |
conversation | Conversation | Persisted record: title, context… |
activations | FoundryActivationRecord[] | This instance's schedules and sleeps |
data | FoundryDataAdapter | The application's adapter, constructed inside the run process |
message | Message | Native Glove user message for this turn, including media parts |
messageInput | string | ContentPart[] | Exactly what processRequest will receive |
messageText | string | Text projection of message |
history | Message[] | Stored messages before this turn ([] without a store) |
messages | Message[] | history plus message |
installations | AgentInstallation[] | { kind, id, config?, accountId? } records from the instance |
store | StoreAdapter | null | The resolved store |
subscriber | SubscriberAdapter | Foundry's event forwarder |
controls | { signal, emit, commands } | Abort signal and custom-event emitter |
The execution context
configure, spawn, run and call handlers receive a FoundryExecutionContext instead. It shares the identity and message fields (definitionId through messages, request, installations) and adds:
| Field | What it is |
|---|---|
glove | The assembled runnable (after build, in run/spawn) |
signal, emit | Run abort signal and custom-event emitter |
onCleanup(fn) | Register run-scoped teardown; runs in reverse order even if assembly fails |
invoke(name, input) | Call one of this run's calls; input and output are validated |
workingEnvironment, vfs, repl | Present when those fields are configured |
goals, facts, forms | Runner handles when guidance is configured |
defaultRun(message?), spawn(message?) | run only: the standard loop, or your spawn path |
The execution context does not carry agentInstance, conversation, activations, data, store or controls. Read instance data in a resolver (a calls resolver can close over it, as below), and use ctx.emit instead of controls.emit.
Assembly order for one run
Before a run starts, the host reloads the instance and conversation from the data adapter, checks they belong together and share a workspace, and enqueues the run. Inside the run process Foundry then:
- Loads
foundry.application.ts, callsstore(or the application'sconversationStore), readshistory, and builds the assembly context. - Resolves
model,systemPrompt,displayManager,compactionLimit,compactionInstructionsandmaxTurnsconcurrently, then constructs the Glove agent. - Resolves
tools,hooks,skills,subagents,memory,inboxes,layers,subscribers,calls,playbooks,schedules,mesh,workingEnvironmentandreplconcurrently. - Reconciles
schedules(every run) andplaybooks(when the field is defined) onto the instance. - Folds the built-in
glove_foundry_*tools, then outbound-transmission tools of installed apps, then yourtools, hooks, skills and subagents. - Mounts the working environment and REPL. A REPL with
programmaticToolswaits until step 12. - Resolves
facts,goals,formsandcontextProvidersand mounts them. - Registers
calls, folding each as a tool unlessexposeToAgent: false. - Mounts
subscribers, then runs each layer'ssetup. - Mounts the instance's installations — shared tools and app contributions in installation order, then every installed MCP server through
mcpAdapter— followed by memory profiles, inbox items and mesh. - Calls
configure(agent, ctx). - Mounts the programmatic REPL, so it sees every tool mounted so far.
- Calls
build(agent, ctx); its return value (or the same agent) is what runs. - Executes
runif defined, otherwisespawn, otherwise Glove'sprocessRequestwith the inbound message. - Finishes: a run that called sleep ends
suspended;onCleanupcallbacks run in reverse, then layer and subscriber teardown.
Each resolver emits foundry.assembly.resolve.start/complete events, so slow assembly is visible in the run trace in the inspector.
Message-dependent assembly
The same definition picks a vision model only when an image is attached, writes its prompt from instance data, and exposes a refund tool only to instances whose context allows it — and only on turns that mention refunds.
import { type GloveFoldArgs } from "glove-core";
import { createAdapter } from "glove-core/models/providers";
import { Effect } from "effect";
import { defineAgent } from "glove-foundry";
import { z } from "zod";
import { FileStore } from "../../lib/file-store.js"; // durable transcript — see Persistence
const lookupOrder: GloveFoldArgs<{ orderId: string }> = {
name: "lookup_order",
description: "Look up an order by its id",
inputSchema: z.object({ orderId: z.string() }),
async do({ orderId }) {
return { status: "success", data: { orderId, status: "shipped" } };
},
};
const issueRefund: GloveFoldArgs<{ orderId: string; amountCents: number }> = {
name: "issue_refund",
description: "Refund part or all of an order",
inputSchema: z.object({ orderId: z.string(), amountCents: z.number().int().positive() }),
requiresPermission: true, // pauses the run for an approval
async do({ orderId, amountCents }) {
return { status: "success", data: { orderId, refundedCents: amountCents } };
},
};
export default defineAgent({
description: "Answers customer questions about orders",
store: ({ conversationId }) => FileStore.open(conversationId),
model: (_agent, { message }) => {
const hasImage = message.content?.some((part) => part.type === "image") ?? false;
return createAdapter({
provider: "openrouter",
model: hasImage ? "openai/gpt-4.1" : "openai/gpt-4.1-mini",
stream: true,
});
},
systemPrompt: (_agent, { agentInstance, conversation, history }) =>
[
`You are the support agent for ${String(agentInstance.context.brand ?? "our store")}.`,
`Conversation "${conversation.title ?? conversation.id}" has ${history.length} earlier messages.`,
].join("\n"),
tools: (_agent, { agentInstance, messageText, controls }) => {
const tools: GloveFoldArgs<any>[] = [lookupOrder];
if (agentInstance.context.canRefund === true && /refund/i.test(messageText)) {
tools.push(issueRefund);
}
// Custom events land in the run trace as agent.support.tools.selected
controls.emit({ type: "support.tools.selected", data: { names: tools.map((t) => t.name) } });
return tools;
},
// Resolvers may return a Promise or an Effect.
compactionLimit: (_agent, { history }) =>
Effect.succeed(history.length > 200 ? 60_000 : 100_000),
maxTurns: 25,
});agentInstance.context is whatever you stored when creating the instance ({ brand: "Acme", canRefund: true }); change it with configureAgent and the next run assembles differently. See Definitions & instances. Data passed to emit is retained in observability as is, so never emit secrets.
configure, calls and a custom run
configure mounts anything a field cannot express and registers its teardown. run takes over execution: here a typed payload takes a deterministic fast path with no model call, and everything else goes through the normal loop with an enriched user turn.
import { createAdapter } from "glove-core/models/providers";
import { defineAgent, defineCall } from "glove-foundry";
import { z } from "zod";
import { FileStore } from "../../lib/file-store.js"; // durable transcript — see Persistence
// your code: a client that holds a connection open for one run
function connectCrm(workspaceId: string) {
return {
async findCustomer(email: string) { return { email, plan: "pro", workspaceId }; },
async close() {},
};
}
const statusRequest = z.object({ kind: z.literal("order-status"), orderId: z.string() });
export default defineAgent({
description: "Support agent with a deterministic fast path",
store: ({ conversationId }) => FileStore.open(conversationId),
model: () => createAdapter({ provider: "openrouter", model: "openai/gpt-4.1-mini", stream: true }),
systemPrompt: "You are a concise customer-support agent.",
calls: (_agent, { agentInstance }) => [
defineCall({
name: "order_status",
description: "Return the status of one order",
input: z.object({ orderId: z.string() }),
output: z.object({ orderId: z.string(), status: z.string(), region: z.string() }),
// Closes over assembly-time instance data.
handler: ({ orderId }) => ({
orderId,
status: "shipped",
region: String(agentInstance.context.region ?? "us"),
}),
}),
],
configure: async (agent, ctx) => {
const crm = connectCrm(ctx.workspaceId);
ctx.onCleanup(() => crm.close()); // register right after acquiring
agent.fold({
name: "find_customer",
description: "Find a customer in the CRM by email",
inputSchema: z.object({ email: z.string() }),
async do({ email }) {
return { status: "success", data: await crm.findCustomer(email) };
},
});
agent.addContextProvider(() => `Current time: ${new Date().toISOString()}`);
},
run: async (_agent, ctx) => {
const status = statusRequest.safeParse(ctx.request.payload);
if (status.success) {
ctx.emit({ type: "support.fast-path", data: { orderId: status.data.orderId } });
return ctx.invoke("order_status", { orderId: status.data.orderId });
}
return ctx.defaultRun([
{ type: "text", text: `${ctx.messageText}\n\n(arrived via ${ctx.request.source?.kind ?? "direct"})` },
]);
},
});defaultRun(message?)runs Glove'sprocessRequest; without an argument it uses the inbound message. Its return value is the final assistant text.- Whatever
runreturns becomes the run'soutput(JSON-serialized). A fast path that skipsdefaultRunwrites nothing to the conversation store. - Inside
run,ctx.spawn(message?)calls yourspawnhook when one is defined and falls back todefaultRun. onCleanupcallbacks run after the run finishes, fails or is cancelled. A failing callback does not stop the others; the errors are reported together.
build
build(agent, ctx) runs after everything is mounted and receives the assembly context. Return a wrapped or different IGloveRunnable to change what executes, or return nothing to keep the assembled agent. Prefer configure for mounting; reach for build only when you need to replace the runnable itself.
Splitting the definition across files
Larger agents keep agent.ts small by importing colocated pieces — tools/*.tool.ts, apps/*.app.ts, mcp/*.mcp.ts, memory/*.memory.ts, layers/*.layer.ts, subscribers/*.subscriber.ts — and passing them to composeAgent(...) in components. File names become ids; see Project structure for the conventions and Tools, MCP & memory for how each piece is mounted.