Layered agents
One agent that talks to a person, remembers them, writes code and browses the web gets slow and careless. Every tool and instruction it carries is read on every call, and the person waits while it works. Split it into layers instead: an agent that talks and remembers, and sibling agents that do the work and report back. In Foundry that split is one field, delegates.
Three questions decide the layers
Put two jobs in different agents when the answers to these questions differ. Then give each agent only the tools and access its job needs.
| Interactor (layer 1) | Worker (layer 2) | |
|---|---|---|
| How fast must it reply? | Now. A person is waiting, maybe on a call. | Minutes are fine. Nobody is watching it type. |
| What context must it keep? | The person: who they are, what they like, what they are waiting on. | One task. A fresh conversation per delegation. |
| How long does it run? | One turn, or one call. | As long as the task takes, across runs if it sleeps or asks. |
| What can it reach? | Personal memory, the person, its delegates. | Its sandbox, browser or APIs, and a question back to the interactor. |
| Which model? | Small and fast, or a speech-to-speech model. | Stronger and slower, chosen per job. |
Long-running or scheduled work is still a worker: it can sleep or be woken by a schedule, and its result comes back the same way.
The pattern
The interactor is the only agent the person talks to. It understands what they want, turns it into a task another agent can finish without having been in the room, and keeps talking. Workers do the task. When a worker is missing something, it asks the interactor, which answers from what it knows or asks the person. Step through it:
- The interactor is the one agent the person talks to. It runs on a fast model and keeps their context: who they are, what they like, what they are waiting on.
glove_foundry_delegatereturns at once. Foundry opens a conversation for this task on the person's owncoderinstance and starts a run there.- The interactor's turn ends in a second. Nobody waits on the slow work.
- The coder cannot tell which language the person wants, so it calls
glove_foundry_ask. The question goes to the conversation that delegated. The coder's run waits up toanswerWaitMs, keeping its sandbox. - The question arrives as a
<delegation status="question">message. Memory already has the answer, so the interactor callsglove_foundry_replywithout bothering the person. If it did not know, it would ask them first. The answer lands in the waiting run as the tool's result. - The coder's final reply is the result. Foundry delivers it to the interactor's conversation: straight into a run that is listening, such as a call, or as a new turn once the conversation is free.
- The interactor passes on what matters. Open delegations sit in its runtime context the whole time, so “how is that going?” needs no tool call.
Build it
These files come from examples/foundry-layered-assistant, which runs offline with pnpm verify. The interactor names its delegates by route:
import { composeAgent, defineAgent } from "glove-foundry";
import { FileStore } from "../../lib/file-store.js";
import { callModel, interactorModel } from "../../lib/models.js";
import { call, isCall } from "./call.js";
import personal from "./personal.memory.js";
import { interactorPrompt } from "./prompt.js";
/** Layer 1. This file only assembles; the prompt, memory and call live beside it. */
export default defineAgent({
description: "Talks with the person, remembers them, and hands work to the coder and researcher",
components: composeAgent(personal),
model: (_agent, context) => (isCall(context) ? callModel() : interactorModel()),
systemPrompt: (_agent, context) => interactorPrompt(isCall(context)),
store: ({ conversationId }) => FileStore.open(conversationId),
memory: [personal],
delegates: [
{ agent: "coder", description: "Writes and runs code in its own workspace. Takes minutes. Give it the whole task." },
{ agent: "researcher", description: "Reads the web and reports back with sources. Takes a minute or two." },
],
run: (agent, context) => (isCall(context) ? call(agent, context) : context.defaultRun()),
});A worker is an ordinary definition. Nothing in it says who may delegate to it; it gets a task as its first message and its final reply is the result.
import { defineAgent } from "glove-foundry";
import { FileStore } from "../../lib/file-store.js";
import { workerModel } from "../../lib/models.js";
import { coderPrompt } from "./prompt.js";
import { coderWorkspace } from "./workspace.js";
/** Layer 2. No memory of the person and no way to message them. */
export default defineAgent({
description: "Writes and runs code in a sandboxed workspace",
model: () => workerModel("coder"),
systemPrompt: coderPrompt,
store: ({ conversationId }) => FileStore.open(conversationId),
workingEnvironment: coderWorkspace,
answerWaitMs: 120_000, // wait this long in-run for an answer to glove_foundry_ask
});What delegates gives you:
glove_foundry_delegate({ to, task })on the interactor. It returns a delegation id at once. Its description lists the delegates and what each is for, so write those descriptions for the model that has to choose.glove_foundry_reply({ delegationId, message })on the interactor, to answer a worker's question or send more instructions about a task.glove_foundry_ask({ question })on the worker, for the one thing it cannot decide alone.- One worker instance per interactor instance and worker route (
maya.coderfor the instancemaya), created on first use. It starts with a copy of the interactor instance'scontext, so a per-person id is there, anddelegatedBynames who it works for. - One conversation per delegation on that worker. Its id is the delegation id, and a follow-up on the same delegation continues it, files and all.
Write the parts beside agent.ts
agent.ts is where a definition is assembled. The parts go in their own modules: the prompt is a function in prompt.ts, memory is a *.memory.ts file, the workspace and the call each have a file, and the model choice for every layer is in lib/models.ts. Each part can be read and tested on its own, and the definition stays a page you can take in at a glance. See Project structure.
agents/
assistant/
agent.ts assembly: model per surface, memory, delegates, run
prompt.ts how the interactor talks and when it delegates
personal.memory.ts what it knows about the person (glove-memory on SQLite)
call.ts the voice call: room and greeting
coder/
agent.ts prompt.ts workspace.ts
researcher/
agent.ts prompt.ts workspace.ts the workspace adds env:fetch
lib/
models.ts one model choice per layerWhen a worker needs to ask
A worker that guesses wastes the whole task. glove_foundry_ask sends one question to the conversation that delegated, where it arrives as a message:
<delegation id="delegation_9f2c…" agent="coder" status="question">
Task: The user asked: Write me a slugify function, please.
The coder agent needs an answer to continue: Which language should the code be in?
If you already know the answer, reply with glove_foundry_reply. Otherwise ask the user, then reply.
</delegation>The interactor answers with glove_foundry_reply: straight away if memory or the conversation already has the answer, or after asking the person. Meanwhile the worker's ask waits in the same run for up to answerWaitMs (90 seconds by default), so a worker holding a browser session or a running sandbox keeps it and gets the answer as the tool's result. If no answer comes in time, the tool tells the worker to end its turn; the run finishes as suspended, and the answer later starts a new run in the same conversation. Give workers a durable store so that run has the history.
Where results go
A worker's final reply is the result. Foundry sends it, every question and any failure back to the conversation that delegated:
- To a run that is listening. A run can take updates itself with
ctx.onDelegationUpdate(handler).voiceCalldoes, so a result that lands mid-call is spoken at the next pause. - Otherwise, as a new turn. Updates wait until no run holds the conversation, then start one run with everything that arrived meanwhile, as
<delegation>messages withsource.kind"delegation".
While anything is open, the interactor's runtime context lists it (“delegation_… (coder): working on …”), so it can answer “how is that going?” without a tool call. A failed or cancelled worker run reports back as status="failed"; a run cancelled by steering does not, because its replacement continues the task.
Siblings, not children
Delegates are named by route rather than imported. The interactor's process never loads a worker's code, so a fast voice agent does not start up carrying a sandbox SDK, and each agent's access is decided in its own definition. Pick the boundary by the job:
| You want… | Use | Why |
|---|---|---|
| A fast agent that hands off slow work and hears back | delegates | Separate process, instance and conversation per task. Results and questions come back. |
| A bounded sub-task answered inside this turn | defineSubagent | Runs inside the parent's tool call; the parent waits for it. |
| Start separate work and never hear back | glove_foundry_spawn | Fire and forget, with a run id in the trace. |
| Peers that talk while both are running | mesh | Messages between live runs over your transport. |
| Fan out from code you control | the typed client | Deterministic orchestration in a run. |
Keep access where it belongs
- The person stays with layer 1. Workers have no tool that reaches them; a question goes through the interactor, which decides whether to ask.
- Memory stays with layer 1. Put personal memory on the interactor only. It passes on what a task needs, in the task.
- Reach stays with the worker that needs it. The coder has a sandbox, the researcher has the web. Neither has the other's.
- Approvals happen where the risk is. A worker's
requiresPermissiontool pauses on an approval record like any other run. - Instance context is copied, so keep secrets out of it. Use it for ids; credentials belong in adapters and secrets.
How it works
A delegation is a conversation on the worker instance whose context.delegation holds the record. Foundry updates it as the task moves:
interface FoundryDelegation {
id: string; // also the worker conversation's id
to: string; // worker route, e.g. "coder"
workerAgentId: string; // e.g. "maya.coder"
workspaceId: string;
from: { definitionId: string; agentId: string; conversationId: string; runId: string };
task: string;
status: "working" | "waiting" | "completed" | "failed" | "cancelled";
question?: string; // while waiting
result?: string;
error?: string;
undelivered?: "result" | "question" | "failed"; // owed to the delegating conversation
pendingReplies?: string[]; // owed to the worker
createdAt: string;
updatedAt: string;
}| Moment | What Foundry does | Event on the trace |
|---|---|---|
glove_foundry_delegate | Creates the worker instance if needed and the delegation conversation, then queues the worker run with the task wrapped in <delegated-task>. | delegation.started |
glove_foundry_ask | Marks the delegation waiting and delivers the question to the delegating conversation. | delegation.question |
glove_foundry_reply | Checks the delegation was started by this conversation, marks it working, and delivers the reply to the worker: into its waiting run, or as a new run. | delegation.replied |
| Worker run completes | Unless it suspended (asleep or waiting for an answer), records the result and delivers it. | delegation.completed |
| Worker run fails or is cancelled | Records the error and delivers a failed update. | delegation.failed, delegation.cancelled |
| Any delivery | Into a listening run (mode: "live") or a new run (mode: "run"). | delegation.delivered |
- A run that asked and did not get an answer ends with
status: "suspended"andsuspension: { kind: "question", delegationId, question, commandId }. runtime.listDelegations({ agentId?, conversationId?, status? })lists delegations, newest first.- In a resolver,
ctx.delegationsholds the open delegations this conversation started, andctx.delegationis set when the run works on a delegated task. - Delegating to a route that does not exist delivers a
failedupdate rather than failing silently. - An update or reply waiting for a busy conversation is recorded on the delegation (
undelivered,pendingReplies) until it is handed over, so a restart (including the oneglove foundry devdoes on every save) hands it over when the runtime starts again. Use a durable data adapter for this to hold across processes. - Delegations nest: a worker can have
delegatesof its own.