Project structure & conventions
In Foundry the filesystem is the registry for code: a file’s path is its id, and code refers to other definitions by importing them. Runtime records — instances, conversations, installations, schedules — live in a data adapter, not in files. This page lists every file Foundry reads, recognizes or generates.
Standalone layout
npx glove-foundry init my-app creates a project that owns its own package.json. Grown to use every convention, it looks like this:
agents/
support/
agent.ts definition "support" (discovered)
composition.ts composeAgent(...) catalogue (plain module)
tools/lookup-order.tool.ts tool "lookup-order" (discovered)
apps/helpdesk.app.ts application "helpdesk" (discovered)
transmissions/tickets.transmission.ts transmission "tickets" (discovered)
events/ticket-created.event.ts event "ticket-created" (discovered)
predicates/is-urgent.predicate.ts predicate "is-urgent" (discovered)
actions/resolve.action.ts playbook action "resolve" (discovered)
connections/ticket-feed.connection.ts connection "ticket-feed" (discovered)
subscriptions/urgent.subscription.ts playbook subscription "urgent" (discovered)
mcp/knowledge.mcp.ts MCP server "knowledge" (discovered)
memory/customer.memory.ts memory profile "customer" (discovered)
layers/request-context.layer.ts layer "request-context" (discovered)
subscribers/audit.subscriber.ts subscriber "audit" (discovered)
schedules/follow-up.ts defineSchedule value (plain module)
topology.ts accounts and routes (plain module)
lib/ your helpers (stores, clients, models)
src/client.ts scripts that call the runtime
foundry.application.ts runtime infrastructure: data, stores, auth
foundry.config.ts runtime process settings
.foundry/routes.d.ts generated route types (commit it)
.foundry/manifest.json generated manifest (git-ignored)
.env.example .env.local environment; .env.local is git-ignored
eslint.config.js tsconfig.json package.jsonThe generated package.json is "type": "module", requires Node >=22, and has four scripts: dev (glove foundry dev), start (glove foundry start), lint (eslint .) and typecheck (tsc --noEmit). Its tsconfig.json includes agents, lib, src, both root config files and .foundry/routes.d.ts. The minimal template starts with one agent (agents/assistant/) and one tool; travel-concierge has one example of each convention.
Next.js layout
npx glove foundry init . inside a Next.js app (detected from next.config.* or a next dependency, or forced with --target nextjs) joins the app instead of taking it over. Existing files are never overwritten.
| Path | What it is |
|---|---|
foundry/agents/** | The agents, same conventions as above |
foundry/foundry.application.ts | Data adapter, accounts, routes |
foundry/package.json | { "type": "module" } scoped to the agent tree, so the app keeps its own module system |
foundry.config.mts | Sets agentsDir: "foundry/agents" and applicationFile: "foundry/foundry.application.ts" |
lib/foundry.ts | createFoundryClient<FoundryRoutes> with baseUrl: process.env.FOUNDRY_URL ?? "http://127.0.0.1:4141" |
app/api/<agent>/route.ts | Example POST handler: create instance, conversation, send, run.wait() |
foundry/README.md, foundry.env.example | Guide and environment template |
package.json | Gains foundry:dev, foundry:start and the Glove dependencies; nothing else changes |
No tsconfig.json or ESLint config is generated for this target; the app’s own apply. The runtime is a separate process (pnpm foundry:dev next to pnpm dev) and the app reaches it over HTTP. See Next.js integration.
File conventions
Agents. Foundry scans agentsDir (default agents) for .ts, .tsx, .mts, .js and .mjs files (not .d.ts):
| File | Agent id |
|---|---|
agents/support/agent.ts | support |
agents/teams/billing/agent.ts | teams/billing |
agents/support.agent.ts | support (single-file form; prefer a folder) |
agents/agent.ts | default |
An agent file default-exports defineAgent(...). Two files resolving to the same id fail discovery with Duplicate Foundry agent id.
Agent-local definitions. For each agent file, Foundry recursively scans the folder that contains it and binds every file whose name (before the extension) ends in one of these suffixes. Each must default-export one definition of the matching kind.
| Suffix | Default export | How it is used |
|---|---|---|
*.tool.ts | defineSharedTool(...), or a bare Glove tool body | Catalogue → installed per instance |
*.app.ts | defineApp(...) (alias of defineAgentApplication) | Catalogue → installed per instance |
*.mcp.ts | defineMcp(...) | Catalogue → installed per instance |
*.memory.ts | defineMemory(...) | Catalogue + memory field |
*.layer.ts | defineLayer(...) | Catalogue + layers field |
*.subscriber.ts | defineSubscriber(...) | Catalogue + subscribers field |
*.transmission.ts | defineTransmission(...) | Imported by apps and playbooks |
*.event.ts | defineTransmissionEvent(...) | Imported by transmissions and playbooks |
*.predicate.ts | defineTransmissionPredicate(...) | Imported by transmissions and playbooks |
*.action.ts | definePlaybookAction(...) | Imported by composePlaybook directives |
*.connection.ts | defineConnection(...) | Imported by an app’s connections |
*.subscription.ts | definePlaybookSubscription(...) | Background playbook policy |
Not conventions. These are ordinary modules you import; Foundry never looks for them: composition.ts, schedules (defineSchedule values go in the schedules field — there is no .schedule.ts suffix), instances (defineAgentInstance), topology records (defineAccount, defineInboundRoute, defineOutboundRoute, defineBinding), working environments and REPLs, inbox loaders, typed calls and subagents. Root-level tools/, applications/, mcp/ or memory/ folders are not discovered: capabilities are agent-local.
How paths become ids
A local definition’s id is its path relative to the agent’s folder, minus the extension and suffix. If the first folder is one of the conventional names (tools, apps, mcp, memory, layers, subscribers, transmissions, events, predicates, actions, connections, subscriptions, playbooks), it is dropped. Deeper folders stay.
File (under agents/support/) | Id |
|---|---|
tools/lookup-order.tool.ts | lookup-order |
tools/calendar/today.tool.ts | calendar/today |
lookup-order.tool.ts | lookup-order |
billing/refund.tool.ts | billing/refund |
apps/helpdesk/search.tool.ts | helpdesk/search |
The id is what installations reference ({ kind: "tool", id: "calendar/today" }). The name the model sees is the tool’s name. A bare tool body (an object with description, inputSchema and do, but no name) gets its name from the id: / becomes __ and - becomes _, so calendar/today is the tool calendar__today.
import { z } from "zod";
// No defineSharedTool, no name: Foundry names it "calendar__today" from the path.
export default {
description: "Return today's date in ISO format",
inputSchema: z.object({}),
async do() {
return { status: "success" as const, data: new Date().toISOString().slice(0, 10) };
},
};Id rules
- Ids are lowercase path segments of letters, digits and hyphens, starting with a letter, separated by
/(^[a-z][a-z0-9-]*(/[a-z][a-z0-9-]*)*$). Name files in kebab-case:lookupOrder.tool.tsproduces the idlookupOrder, and any run of an instance that installs it fails withInvalid Foundry tool id. - Agent ids are validated at discovery; an invalid folder name stops startup.
- Do not pass
idto file-routed helpers. The option still exists for programmatic definitions, but a mismatch with the file route throws, and the ESLint preset rejects it. - A definition that is not default-exported from a convention file inside an agent folder is never bound. Using it fails with
Foundry tool identity has not been bound yet(or the matching kind). - Runtime records keep explicit ids because people and adapters create them: instances, conversations, accounts, routes and bindings. Code refers to them by value (
{ account: supportAccount }), never by copying the string.
foundry.application.ts
Runtime infrastructure: where data lives and who may call the HTTP surface. It must default-export defineApplication(...). Foundry loads it in the runtime process and in every agent run’s worker process, so anything it creates must be safe to construct more than once. Without the file, Foundry uses an empty application with in-memory data.
| Option | Purpose |
|---|---|
name | Required, non-empty |
data | FoundryDataAdapter for instances, conversations, installations, playbooks, subscriptions, activations, workspace entries, inbox, tasks and VFS snapshots. Default MemoryFoundryDataAdapter (lost on restart); FileFoundryDataAdapter for one host |
conversationStore | (scope) => StoreAdapter used for transcripts when a definition has no store of its own |
requestAuthorization | { identifier, challenge?, authorize(request) } returning Effect<boolean>; gates every HTTP request and is required to bind a non-loopback host |
accounts, routes, bindings | Integration topology from defineAccount, defineInboundRoute/defineOutboundRoute, defineBinding; duplicate ids throw |
provisioner | Your strategy for playbook subscriptions with provisioning.mode = "custom" |
daemon | stationDaemon(...) from glove-foundry/station for browser/sandbox resources |
services | Effect Layer replacing the in-memory AccountDirectory, TopologyStore and EventStore |
import { FileFoundryDataAdapter, defineAgentInstance, defineApplication, install } from "glove-foundry";
import support from "./agents/support/agent.js";
import lookupOrder from "./agents/support/tools/lookup-order.tool.js";
const supportEu = defineAgentInstance(support, {
id: "support-eu",
workspaceId: "acme",
installations: [install(lookupOrder, { region: "eu" })],
});
export default defineApplication({
name: "Support desk",
// Seeds are inserted only when the file has no instance with that id.
data: new FileFoundryDataAdapter({ file: ".data/foundry.json", agents: [supportEu] }),
});Agent definitions do not belong here; they are discovered from agents/. See Persistence and Deploy & security.
foundry.config.ts
Settings for the runtime process. Foundry looks for foundry.config.ts, .mts, .js or .mjs in the project root; it must default-export a value. defineConfig (from glove-foundry/config) makes unknown keys a type error. CLI flags --port and --host override the file.
| Key | Default | Meaning |
|---|---|---|
agentsDir | "agents" | Where agents are discovered |
applicationFile | "foundry.application.ts" | Path of the application file |
server.host | "127.0.0.1" | Non-loopback requires requestAuthorization |
server.port | 4141 | HTTP API, inspector and WebChat |
server.messageBodyBytes | 1 MB (1048576) | JSON limit for message-bearing endpoints; integer from 1048576 to 134217728 |
execution.maxConcurrent | 5 | Runs executing at once |
execution.maxAttempts | 1 | Attempts per run |
execution.retryBackoffMs | 1000 | Delay between attempts |
execution.pollIntervalMs | 100 | Execution polling interval |
execution.idlePollIntervalMs | 1000 | Deprecated; kept for source compatibility |
observability.maxEvents | 10000 | Events retained for traces |
branding.name / label / descriptor / accent | Glove Foundry / — / Inspector / — | Inspector shell text and a six-digit hex accent |
strictFileRoutes | true | Reject an agent module whose exported id differs from its file route |
import { defineConfig } from "glove-foundry/config";
export default defineConfig({
server: { port: 4141, messageBodyBytes: 40 * 1024 * 1024 },
execution: { pollIntervalMs: 100, maxConcurrent: 4 },
});Generated files
Every glove foundry dev and start rewrites two files in .foundry/ before the server listens:
// Generated by Glove Foundry. Do not edit.
export type FoundryRoutes = {
readonly "support": typeof import("../agents/support/agent.js");
};Pass it to the client so route names are checked at compile time: createFoundryClient<FoundryRoutes>() makes foundry.agent("suport") a type error. It is a type-only import, so no agent code reaches the calling app. Commit it (the starter does), so typecheck works before the first run.
.foundry/manifest.json (version: 1) describes each discovered agent: id, description, file, tags, handler ("glove", or "custom" when run/spawn is set), the statically known names of tools, hooks, skills, subagents, memory, layers, subscribers, calls, schedules and playbooks, flags for inboxLoader, mesh, workingEnvironment and repl, and lazy — the fields given as resolvers, whose contents are only known per run. The starter git-ignores it. GET /api/manifest serves the same data plus each definition’s capability catalogue.
The ESLint preset
glove-foundry/eslint default-exports a flat-config object named glove-foundry/recommended that applies to **/*.{ts,tsx,mts,js,mjs}. The starter’s config:
import tseslint from "typescript-eslint";
import foundry from "glove-foundry/eslint";
export default [
{ ignores: [".foundry/**"] },
...tseslint.configs.recommended,
foundry,
];Rule (all error) | Rejects | Use instead |
|---|---|---|
glove-foundry/no-agent-contracts | input or output in defineAgent({…}), and export const input/output | Nothing: Foundry owns the request and result contracts; use defineCall for typed operations |
glove-foundry/no-file-definition-id | An id property in defineAgent, defineSharedTool, defineApp, defineAgentApplication, defineMcp, defineMemory, defineLayer, defineSubscriber, defineTransmission, defineTransmissionEvent, defineTransmissionPredicate, definePlaybookAction, defineConnection, definePlaybookSubscription | The filename |
glove-foundry/no-raw-definition-references | accountId in install(def, config); transmissionId, applicationId, routeId, routeIds, applicationAccountId and string-literal event/action inside composePlaybook | install(def, config, { account }); imported transmission, application, route(s), applicationAccount, event, action values |
The plugin is also exported as foundryEslintPlugin and the preset as foundryEslintConfig.
Catalogue, installation, or definition field?
components: composeAgent(...) is a catalogue: it says which parts an instance may use. Nothing in it reaches the model until something else selects it. This table is the whole rule:
| Part | Goes in | Mounted on a run when |
|---|---|---|
Plain Glove tool (name, description, inputSchema, do) | tools field | Always, on every instance |
Shared tool (*.tool.ts) | composeAgent | The instance has a tool installation for its id |
Application (*.app.ts) | composeAgent | The instance has an application installation; its outbound transmissions become tools named glove_app_<app>__<transmission>_send, plus any tools its install hook returns |
MCP server (*.mcp.ts) | composeAgent | The instance has an mcp installation; the definition must also set mcpAdapter |
Memory profile (*.memory.ts) | composeAgent and the memory field | Listed in memory (directly or via configureMemory); no installation |
Layer (*.layer.ts) | composeAgent and the layers field | Listed in layers (or configureLayer); no installation |
Subscriber (*.subscriber.ts) | composeAgent and the subscribers field | Listed in subscribers; a raw SubscriberAdapter object needs no catalogue entry |
Schedule (defineSchedule) | schedules field | Reconciled into an activation on the instance’s next run |
Typed call (defineCall) | calls field | Always; also exposed as a tool unless exposeToAgent: false |
| Hooks, skills, subagents | hooks, skills, subagents fields | Always |
Built-in glove_foundry_* tools | Nothing | Always: spawn, schedule, schedules, sleep, background, transmit, conversations, workspace, shared_inbox, tasks, environment |
Selecting a memory profile, layer or subscriber that is missing from composeAgent fails the run (Unknown agent memory profile, references unknown layer). Installing an id that is missing from it is rejected (Unknown Foundry tool capability). Installations are added with install(def, config?, { account? }) in defineAgentInstance, installations on client.agent(route).create, client.install, or PUT /api/installations. Walk through it in the tutorial; every field is in Tools, MCP & memory.