Personal agent
A personal agent is the assistant you text: it remembers you, reminds you, reads and answers your email, browses for you and writes documents, often while you are asleep. Most of its weight sits in the system layer. It needs an identity per person, time (schedules and sleep), arrival (a chat channel that wakes it) and oversight (it asks before anything it cannot undo). Foundry provides all four; Kuiper provides the mailbox, the browser and somewhere to run.
- The chat channel: a transmission and a connection (Telegram long polling, say)
- The system prompt and the memory schema: what is worth remembering about a person
- A browser adapter for your provider, if it browses
- Domain tools: calendar, notes, whatever your life runs on
How it fits together
At 23:40 you text “remind me to call the dentist when they open, and book the earliest slot if you can”. The Telegram connection, which Foundry keeps running while an instance has the Telegram app installed and a playbook that uses it, hands the update to context.receive. The transmission authenticates and normalises it, the playbook routes it to your instance, and the chat id becomes the conversation's thread key, so the same chat always lands in the same conversation. With awaitCompletion: true the connection waits for the run to finish before it accepts your next message, so turns stay in order.
The run assembles a fresh Glove from the definition, your instance and the message. The store reloads the conversation from /data, pinned context tells the model your timezone and preferences, and memory tools let it look up which dentist you mean. It calls glove_foundry_sleep until 08:30 and the run ends. At 08:30 the same instance wakes in the same conversation with the resolution message. It opens the clinic's booking page through the browser, finds a slot, and calls the booking tool, which is permission-gated. Foundry turns that into an approval record; you approve it from the inspector or from a button your channel renders, and the booking goes through. The reply goes back to Telegram through the transmission's outbound tool.
Skeleton
The agent folder. Each file is one definition and its filename is its id:
agent.ts the agent: model, prompt, store, memory, tools, schedules
composition.ts composeAgent(telegram app, memory)
apps/telegram.app.ts defineApp({ inbound: [telegram], outbound: [telegram], connections: [poller] })
transmissions/telegram.transmission.ts updates in, messages out (see Apps & transmissions)
connections/telegram.connection.ts long polling, supervised by Foundry
memory/assistant.memory.ts entity, episodic, resources and pinned context, on SQLiteimport { createAdapter } from "glove-core/models/providers";
import { defineAgent, defineSchedule } from "glove-foundry";
import { mountBrowser } from "glove-execution";
import { Inbox } from "@kuiper-sh/inbox";
import { inboxTools } from "@kuiper-sh/inbox/glove";
import { FileStore } from "../../lib/file-store.js"; // the starter's durable transcript store
import { components } from "./composition.js";
import assistantMemory from "./memory/assistant.memory.js";
// With no apiKey, Kuiper's SDK reads KUIPER_GATEWAY_KEY.
const inbox = new Inbox({ gatewayUrl: process.env.INBOX_URL }); // ${{inbox.BASE_URL}} on Kuiper
const morningBrief = defineSchedule({
name: "morning-brief",
description: "A short brief every morning",
timing: { kind: "cron", expression: "0 7 * * *", timezone: "Europe/London" },
message: "Write the morning brief: unanswered email, today's reminders, anything you are waiting on.",
});
export default defineAgent({
description: "A personal assistant that works while its owner doesn't",
components,
memory: [assistantMemory],
model: () =>
createAdapter({
provider: "anthropic",
model: "claude-sonnet-5-5",
baseURL: process.env.ANTHROPIC_BASE_URL, // ${{models.ANTHROPIC_BASE_URL}} on Kuiper
apiKey: process.env.KUIPER_GATEWAY_KEY, // ${{models.API_KEY}}
}),
store: ({ conversationId }) => FileStore.open(conversationId),
systemPrompt: [
"You are a personal assistant. Be brief; this is a phone.",
"For reminders and recurring work use glove_foundry_schedule; to wait until a time, glove_foundry_sleep.",
"Look people and past events up in memory before asking.",
].join("\n"),
schedules: [morningBrief],
// The mailbox's tools, with sending gated: Foundry turns the gate into an approval.
tools: () =>
inboxTools(inbox, { mailbox: "assistant" }).map((tool) =>
tool.name === "send_email" ? { ...tool, requiresPermission: true } : tool,
),
async configure(agent, context) {
// Your code: a BrowserAdapter for your provider, for example stationBrowserClient(...)
// against the managed Station, as examples/foundry-operator does.
const adapter = await createBrowserAdapter(context.agentId);
let close = () => adapter.close();
context.onCleanup(() => close());
const browser = mountBrowser(agent, { adapter });
close = () => browser.close();
},
});import { Effect } from "effect";
import { defineConnection } from "glove-foundry";
import telegram from "../transmissions/telegram.transmission.js";
export default defineConnection({
description: "Long-polls Telegram for messages to the bot",
transmissions: [telegram],
connect: (context) =>
Effect.gen(function* () {
yield* context.ready();
let offset = 0;
while (!context.signal.aborted) {
// Your code: Telegram's getUpdates, with the bot token from your account-session adapter.
const updates = yield* Effect.tryPromise(() => getUpdates(offset, context.signal));
for (const update of updates) {
offset = update.update_id + 1;
yield* context.receive({
route: context.routes[0]!,
eventId: String(update.update_id), // a redelivered update never runs twice
threadKey: String(update.message.chat.id), // one conversation per chat
awaitCompletion: true, // one turn at a time per chat
raw: update,
});
}
}
}),
});import { Effect } from "effect";
import { defineMemory } from "glove-foundry";
import { MemorySchema } from "glove-memory/core";
import { createSqliteMemoryAdapters } from "glove-memory/sqlite";
// One namespace per instance, so memory spans all of a person's conversations.
const forInstance = (ctx: { workspaceId: string; agentId: string }) =>
createSqliteMemoryAdapters({
file: "/data/memory.sqlite",
namespace: JSON.stringify([ctx.workspaceId, ctx.agentId]),
schema: new MemorySchema(), // define node classes and episode kinds for your domain
});
export default defineMemory({
description: "Pinned preferences, people, past events and notes",
context: { adapter: (ctx) => Effect.sync(() => forInstance(ctx).context) },
entity: { access: "curator", adapter: (ctx) => Effect.sync(() => forInstance(ctx).entity) },
episodic: { access: "curator", adapter: (ctx) => Effect.sync(() => forInstance(ctx).episodic) },
resources: { access: "curator", adapter: (ctx) => Effect.sync(() => forInstance(ctx).resources) },
});Create one instance per person, with that person's chat id in its context and the Telegram app installed (client.agent("assistant").create(...) or defineAgentInstance). The transmission itself follows the ticket example in Apps & transmissions, and examples/foundry-operator shows the browser, sandbox and memory wiring end to end.
Grow it into layers
Once the assistant writes code, browses and drafts documents, one agent doing it all gets slow and loses track of the person. Split it. Keep this agent as the interactor: fast model, personal memory, the chat channel and calls. Move the heavy work to sibling agents it lists in delegates: a coder with a sandbox, a browser agent, a researcher. They report back to the conversation, and when they need to know something they ask the interactor, which answers from memory or asks the person. See Layered agents and the runnable examples/foundry-layered-assistant.
What not to build
- A reminders table and a cron job that calls your agent. Schedules and
glove_foundry_schedulestore the activation durably and rebuild it on restart;glove_foundry_scheduleslets the agent list, pause and cancel its own. - “Check back later” state.
glove_foundry_sleepends the run and wakes the same instance and conversation at the time you asked for. - A users-to-agents table. Instances are that table, with their own context, installations and conversations.
- A polling loop you keep alive yourself. Connections start when an instance needs them, retry, and report their status in the inspector.
- An approvals table. A permission-gated tool in Foundry becomes an expiring, fail-closed approval record with the exact input.
- A vector store and a “remember” tool. glove-memory keeps people, events and notes with provenance, and pinned context reaches every turn without growing the prompt.
Deploy on Kuiper
kuiper units create assistant, thenkuiper deploy --source . -u assistant --wait. Kuiper detects Foundry and startsglove foundry start --host 0.0.0.0 --port 4141; no Dockerfile.- Add a
requestAuthorizationadapter tofoundry.application.ts. Foundry refuses a public bind without one; see Glove Foundry units. - Persist everything on the unit's volume:
FileFoundryDataAdapterat/data/foundry.json, transcripts and memory under/data. A unit with a volume runs one replica, which is what a single-host data adapter needs. - Make the mailbox (
kuiper inbox create assistant) and wire the gateway in:'KUIPER_GATEWAY_KEY=${{models.API_KEY}}','ANTHROPIC_BASE_URL=${{models.ANTHROPIC_BASE_URL}}','INBOX_URL=${{inbox.BASE_URL}}'. A unit gets one gateway key for every service its variables reference. - Store the bot token as a secret (
kuiper secrets set TELEGRAM_BOT_TOKEN=-) and bind it with${{secrets.TELEGRAM_BOT_TOKEN}}; your account-session adapter reads it, never the model. - For a browser, use Kuiper's: it speaks Steel's API, and the unit gets
${{browsers.BASE_URL}}and${{browsers.API_KEY}}. Sessions are metered by the second and held to your budgets.
Long waits belong in glove_foundry_sleep: runs time out after five minutes. The unit's Inspect card shows every run, conversation and pending approval. Full walkthrough: Deploy on Kuiper.
Go further
- examples/foundry-operator: a personal agent with a persistent browser profile, coding sandboxes and memory on one managed Station. Walkthrough: Browser & sandbox agent.
- Kuiper's email agent example: a unit, a mailbox, its webhook and the model gateway, step by step.
- Build the parts one at a time: make it remember, make it a system, put it in production.
- Reference: Playbooks, schedules & sleep, Apps & transmissions, Memory, Approvals.