Make it remember
State is everything that outlives one model call. A harness needs four kinds: the conversation itself, work waiting on something outside, what it has learned, and what it is trying to finish. Glove has a primitive for each, and they all sit behind adapters you can point at your own storage.
The conversation: a store
Every Glove has a StoreAdapter. It holds the messages, token and turn counts, and optionally tasks, permission decisions, inbox items and child stores for subagents. MemoryStore implements all of it in memory. For a store that survives a restart, implement the interface over your database, or extend MemoryStore and write through to disk:
interface StoreAdapter {
identifier: string
getMessages(): Promise<Message[]>
appendMessages(msgs: Message[]): Promise<void>
getTokenCount(): Promise<number>
addTokens(tokens: TokenConsumptionCounter): Promise<void>
getTurnCount(): Promise<number>
incrementTurn(): Promise<void>
resetCounters(): Promise<void>
// Optional. Implement them and the matching feature switches on.
getTasks?(); addTasks?(); updateTask?() // → glove_update_tasks
getPermission?(); setPermission?() // → requiresPermission
getInboxItems?(); addInboxItem?(); updateInboxItem?(); getResolvedInboxItems?() // → inbox
createSubAgentStore?(namespace, durable?) // → subagents with their own history
}glove foundry init generates lib/file-store.ts, a complete file-backed store built exactly this way; copy it for a single-host app. Several hosts need a database-backed store. In step 7, Foundry asks you for one store per conversation and calls it for you.
Work that finishes later: the inbox
"Tag the release once CI is green" can't finish now. With a store that supports the inbox, the agent gets glove_post_to_inbox: it posts the request (blocking or not) and moves on. Anything (a webhook handler, a job, another agent) resolves it later, and the result is injected at the start of the next request:
// The agent earlier called glove_post_to_inbox({ tag: "ci", request: "Tell me when main is green", blocking: true })
const pending = (await store.getInboxItems!()).filter((i) => i.tag === "ci" && i.status === "pending");
for (const item of pending) {
await store.updateInboxItem!(item.id, {
status: "resolved",
response: "main is green at 9f3c2a1",
resolved_at: new Date().toISOString(),
});
}
// Then run a turn so the agent sees it, e.g. agent.processRequest("CI finished.")While a blocking item is pending, every model call is reminded that it may not act on the missing result. That is the whole protocol; there is no polling to write. Mesh delivers messages between agents through this same inbox, and Kuiper's email tools use it to wait for replies.
What it learns: glove-memory
glove-memory splits memory into what each kind is for: an entity graph (people, projects, how they relate), an episodic timeline (what happened when), a resource filesystem (documents the agent files), and pinned context (standing facts every turn should see). Each is an adapter with reader and curator tool sets, and every write carries provenance. The release assistant pins the team's conventions:
import { MemorySchema } from "glove-memory";
import { createSqliteMemoryAdapters } from "glove-memory/sqlite";
import { useContext } from "glove-memory/tools";
const memory = createSqliteMemoryAdapters({
file: "./.data/memory.db", // a durable file, Node 22.13+
namespace: "team:acme/release", // trusted ownership key, never model input
schema: new MemorySchema(),
});
// glove_context_set / _get / _update / _unset, and every pinned entry
// delivered as runtime context before each model call (step 3's seam).
useContext(agent, memory.context);Now "we never ship on Fridays" becomes a pinned entry the agent sees on every call, including next week. For recall over people and history, don't hang every memory tool on the main agent: give a subagent the reader tools and let the main agent ask it. Memory explains why.
What it is trying to finish: facts, forms and goals
Pick the smallest primitive that fits what you need to collect:
| You need… | Use |
|---|---|
| Every argument now, from the model | A plain tool |
| Standing information on every turn | Pinned context: useContext |
| Details people mention early, reused later | Facts: useFacts → record_fact |
| A specific set of validated answers, maybe in steps, then an action | Forms: defineForm + useFormRunner |
| Exact input or a final confirm that must not pass through the model | A display-stack form: pushAndWait |
| Obligations across the whole conversation | Goals: useGoalRunner |
Forms and goals show their live state through runtime context, so the agent never loses track of what is left, however much the person wanders. Goal-oriented agents builds one end to end.
Already built for you
| Primitive | What you get | Instead of building |
|---|---|---|
| Storeglove-core | Messages, token and turn counts, and optional tasks, permissions, inbox and subagent stores behind one interface.StoreAdapter (MemoryStore built in; bring your own) | a conversations table and token accounting |
| Inboxglove-core | Work that can't finish now is posted, resolved later by anything, and injected on the next turn. Blocking items keep the agent from acting on missing results.glove_post_to_inbox (registered when the store supports it) | polling, callbacks and 'check back later' state |
| Memoryglove-memory | An entity graph, an episodic timeline and a resource filesystem with provenance, behind adapters you own (SQLite included).useMemoryReader · useEpisodicReader · useResourcesReader (+ curators) | a vector store and an ad hoc remember tool |
| Formsglove-memory | Validated answers collected over a conversation: steps, branches, skips, undo and executors that act on completion.defineForm(…) · useFormRunner(glove, adapter, { registry, subject }) | a slot-filling state machine |
| Goalsglove-memory | Objectives that survive interruptions, with versioned progress and deferrals.defineGoalProgram(…) · useGoalRunner(glove, adapter, config) | tracking what the agent is supposed to finish inside the prompt |
| Factsglove-facts | Things people mention early, recorded once and reused by forms and goals.useFacts(glove, factStore, source) | asking the user for something they already said |
Next
The assistant remembers the team. To draft real release notes it needs to read diffs, run scripts and produce files. In step 5 it gets a workspace.